Skip to content

refactor: migrate hickory-client to hickory-net#1396

Merged
ibigbug merged 5 commits into
masterfrom
sunset-hickory-client
May 11, 2026
Merged

refactor: migrate hickory-client to hickory-net#1396
ibigbug merged 5 commits into
masterfrom
sunset-hickory-client

Conversation

@ibigbug

@ibigbug ibigbug commented May 11, 2026

Copy link
Copy Markdown
Member

The hickory-client crate has been subsumed into hickory-net per the hickory-dns release notes. No future releases of hickory-client are expected.

Changes

  • Replace hickory-client 0.25 dep in clash-dns with hickory-net 0.26
  • Update feature flags (https-aws-lc-rs, h3-aws-lc-rs, etc.) to point to hickory-net
  • Add missing DnsUdpSocket and AsyncIoTokioAsStd imports in runtime.rs
  • Update dns_client.rs to use Client<DnsRuntimeProvider> (now generic in 0.26)
  • Remove the proto 0.25/0.26 encode/decode roundtrip workaround (was needed because hickory-client pinned to proto 0.25 while the rest used 0.26)
  • Rewrite dns_stream_builder for new sync API: TcpClientStream::new and tls_client_connect now return (Future, BufDnsStreamHandle); await the future before passing stream to Client::new/with_timeout
  • tls_client_connect now takes ServerName<'static> instead of String
  • HTTPS/H3 streams use Client::from_sender pattern
  • Update handler.rs tests to use hickory_net/hickory_proto imports and new 0.26 connection API

Result

hickory-client is fully removed from the dependency tree. The remaining hickory-proto 0.25.2 is pulled by dhcproto (pre-existing, unrelated).

Summary by CodeRabbit

  • Refactor

    • Switched the DNS networking backend and refactored DNS client/request flows for more reliable connections and simpler background handling.
  • Tests

    • Updated test suites and helpers to align with the new networking/client APIs across UDP/TCP/DoT/DoH/DoH3.
  • Chores

    • Remapped feature wiring to the new networking crate, updated DNS-related dependencies, and consolidated test TLS crypto provider setup.

Review Change Stack

The hickory-client crate has been subsumed into hickory-net per the
hickory-dns release notes. No future releases of hickory-client are
expected.

- Replace hickory-client 0.25 dep in clash-dns with hickory-net 0.26
- Update feature flags (https-aws-lc-rs, h3-aws-lc-rs, etc.) to point
  to hickory-net instead of hickory-client
- Add missing DnsUdpSocket and AsyncIoTokioAsStd imports in runtime.rs
- Update dns_client.rs to use Client<DnsRuntimeProvider> (now generic)
- Remove proto 0.25/0.26 encode/decode roundtrip workaround
- Rewrite dns_stream_builder for new sync API: TcpClientStream::new
  and tls_client_connect now return (Future, BufDnsStreamHandle);
  await the future before passing to Client::new/with_timeout
- tls_client_connect now takes ServerName<'static> instead of String
- HttpsClientStream and H3ClientStream use from_sender pattern
- Update handler.rs tests to use hickory_net/hickory_proto imports
  and new 0.26 connection API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR upgrades clash-rs from hickory-client 0.25 to hickory-net 0.26. Cargo manifests are updated to wire feature flags to the new crate; DNS client types are parameterized with DnsRuntimeProvider; request/response handling adapts to the new API shapes; and stream builders for UDP, TCP, DoT, DoH, and DoH3 transports are refactored. Test code across both modules is updated accordingly.

Changes

Hickory DNS Library Upgrade

Layer / File(s) Summary
Cargo Dependency & Feature Updates
clash-dns/Cargo.toml, clash-lib/Cargo.toml
Both aws-lc-rs and ring features are updated to reference hickory-net sub-features instead of hickory-client; primary DNS dependency changed from hickory-client 0.25 to hickory-net 0.26; dhcproto switched from crates.io to git pin.
Runtime Provider Imports
clash-lib/src/app/dns/runtime.rs
RuntimeProvider, TokioHandle, TokioTime, AsyncIoTokioAsStd, and DnsUdpSocket imports moved from hickory_client::proto::runtime to hickory_net::runtime.
DNS Client Imports & Inner Type
clash-lib/src/app/dns/dns_client.rs
Module imports refactored to use hickory_net and hickory_proto types; Inner struct client type changed to client::Client<DnsRuntimeProvider> with simplified background task handle type.
Request/Response Handling in Exchange
clash-lib/src/app/dns/dns_client.rs
exchange constructs DnsRequest directly with random ID assignment when needed and maps responses via op::DnsResponse.into_message(), removing prior encode/decode workaround.
Stream Builder Signature & UDP Transport
clash-lib/src/app/dns/dns_client.rs
dns_stream_builder return type updated to (client::Client<DnsRuntimeProvider>, JoinHandle<()>); UDP branch uses Client::from_sender and spawns the sender task.
TCP Transport Stream
clash-lib/src/app/dns/dns_client.rs
TCP branch refactored to await TcpClientStream::new(...), construct client via Client::new(stream, sender), and spawn background task.
TLS Transport Stream (DoT)
clash-lib/src/app/dns/dns_client.rs
TLS branch converts hostname to rustls::ServerName, awaits TLS connect, builds client via Client::with_timeout, and spawns background task.
HTTPS & H3 Transport Streams
clash-lib/src/app/dns/dns_client.rs
HTTPS branch builds stream via HttpsClientStream::builder(...).build(...).await, creates client via Client::from_sender, and spawns task; H3 stream builder updated where applicable.
Server-side Rustls CertifiedKey
clash-dns/src/handler.rs
CertifiedKey creation now uses rustls::crypto::CryptoProvider::get_default() and loads private key via provider rather than any_supported_type.
DNS Handler Test Updates
clash-dns/src/handler.rs
Test imports refactored to use hickory_net APIs; send_query signature updated to accept Client<TokioRuntimeProvider>; test cases for UDP, TCP, DoT, DoH, and DoH3 refactored with new client construction and background task spawning patterns; minor assertion adjusted.
Enhanced Resolver Test Updates
clash-lib/src/app/dns/resolver/enhanced.rs
Test module imports switched to hickory_net::proto paths; DNS message construction changed from op::Message::new() to op::Message::query() and request metadata ID handling moved to req.metadata.id.
AnyTLS Test Crypto Provider Changes
clash-lib/src/proxy/anytls/...
Test helper now delegates to crate::setup_default_crypto_provider() and uses rustls::crypto::CryptoProvider::get_default() for supported schemes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Watfaq/clash-rs#1395: Modifies Rustls crypto-provider selection and related imports; touches the same crypto-provider setup code.

Suggested labels

dependencies, rust

Poem

🐰 A hop through the hickory tree,
From client to net, 0.26 we see,
Streams and senders find their place,
Tests and TLS all updated with grace,
A little rabbit cheers—hip, hip, hooray! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: migrating from hickory-client to hickory-net across the codebase. It is concise, clear, and directly reflects the main objective of the pull request.
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 docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Switch dhcproto from crates.io 0.14 to latest git ref
(eece41c) which has been updated to use hickory-proto 0.26.1,
eliminating the last duplicate hickory-proto 0.25.x in the
dependency tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

📊 Proxy Throughput Results

Shadowsocks

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
plain 32 MB 3 15252.4 ±3662.7 13106.9 ±3590.0
obfs-http 32 MB 3 14194.9 ±8832.6 15661.1 ±1591.8
obfs-tls 32 MB 3 10917.8 ±1409.3 8495.3 ±5669.3
shadow-tls-v3 32 MB 3 14279.6 ±376.7 15029.3 ±1429.3
v2ray-plugin-ws-tls 32 MB 3 14603.1 ±2187.4 14528.4 ±2196.3

Trojan

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
tcp 32 MB 3 14101.6 ±8848.8 11592.3 ±1804.0
ws 32 MB 3 13201.8 ±8289.4 16180.3 ±1089.2
grpc 32 MB 3 17763.5 ±619.3 16841.6 ±762.8

VMess

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
tcp 32 MB 3 11622.0 ±1978.2 13791.4 ±2324.5
tcp-tls 32 MB 3 15235.2 ±1569.6 16778.5 ±85.6
ws 32 MB 3 17234.0 ±307.2 16227.7 ±1480.1
h2 32 MB 3 17286.1 ±1061.1 15038.1 ±1637.3
grpc 32 MB 3 10876.9 ±2015.6 11591.0 ±1970.4

VLESS

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
tcp 32 MB 3 13033.7 ±828.2 12066.1 ±898.5
ws 32 MB 3 16301.9 ±1859.5 15500.2 ±843.3
h2 32 MB 3 13203.8 ±1567.8 12353.4 ±816.5
grpc 32 MB 3 17028.9 ±9411.2 16646.3 ±587.4

SOCKS5

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
auth 32 MB 3 16683.1 ±4231.0 15119.4 ±1615.7
noauth 32 MB 3 13133.6 ±2475.5 12474.6 ±1365.4

AnyTLS

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
tls 32 MB 3 10248.5 ±2733.4 13069.4 ±1754.6

Hysteria2

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
plain 32 MB 3 14946.8 ±8000.1 13174.9 ±974.9
salamander 32 MB 3 14798.1 ±1033.1 12830.9 ±434.1

TUIC

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
bbr 32 MB 3 17836.5 ±817.5 16374.8 ±876.2
cubic 32 MB 3 10431.9 ±605.0 11322.9 ±1735.4
new_reno 32 MB 3 13521.2 ±3572.9 8521.2 ±3082.5

ShadowQUIC

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
plain 32 MB 3 14926.0 ±8072.1 10126.7 ±3134.2
over-stream 32 MB 3 16402.5 ±2060.6 14224.4 ±887.0

SSH

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
password 32 MB 3 10286.9 ±2241.1 10785.6 ±1276.2
ed25519 32 MB 3 11832.7 ±1856.1 13067.7 ±1115.8

Netem Tests (50 ms delay, 1% packet loss)

Shadowsocks

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
plain-netem 32 MB 3 14720.2 ±425.8 16210.9 ±1382.8

Trojan

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
tcp-netem 32 MB 3 16123.6 ±787.5 14756.3 ±1536.3

Hysteria2

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
plain-netem 32 MB 3 10141.7 ±3371.9 13147.1 ±1477.5

TUIC

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
bbr-netem 32 MB 3 13939.5 ±1310.2 13901.8 ±4100.6

ShadowQUIC

Transport Payload Runs Upload Mbps (±σ) Download Mbps (±σ)
plain-netem 32 MB 3 12776.8 ±683.2 11096.5 ±1042.3

Ran 34 variant(s) in parallel; each direction transfers the full payload.

🖥️ Test Environment

OS Linux 6.17.0-1010-azure
Architecture x86_64
Kernel 6.17.0-1010-azure
CPU AMD EPYC 9V74 80-Core Processor
CPU Cores 4
Memory 15.61 GB
Docker 28.0.4
Rust rustc 1.95.0 (59807616e 2026-04-14)

📎 View full workflow run and download artifacts

Full test log

Download the throughput-results artifact from the workflow run for the full log.

…_default()

Instead of repeating #[cfg(feature = "aws-lc-rs")] / #[cfg(feature = "ring")]
at every call site, use the runtime-installed default provider:

- clash-dns/src/handler.rs: remove the two cfg-gated any_supported_type
  imports; use CryptoProvider::get_default().key_provider.load_private_key()
- clash-lib/src/proxy/anytls/inbound/handler.rs: replace feature-gated
  supported_verify_schemes() body with CryptoProvider::get_default()
- anytls/inbound/{handler,tls}.rs tests: replace local install_crypto_provider()
  helpers that duplicated the feature-gated logic with a call to
  crate::setup_default_crypto_provider()

The single source of truth for feature selection remains the centralized
setup_default_crypto_provider() functions in clash-lib/src/lib.rs and
clash-dns/src/lib.rs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@clash-dns/src/handler.rs`:
- Around line 557-559: The current use of inspect_err with assert!(false, ...)
around stream_future.await is unidiomatic and masks proper error propagation;
remove the inspect_err call and simply await the future with the trailing ? so
the error propagates (i.e., assign stream = stream_future.await?), or if you
want a custom panic use .expect(...) on the awaited result instead; update the
expression that binds stream (where stream_future.await is used and inspect_err
is called) accordingly.

In `@clash-lib/src/app/dns/resolver/enhanced.rs`:
- Around line 858-868: Remove the redundant inner import inside the
test_bad_labels_with_custom_resolver test: delete the line "use
hickory_net::proto::{op, rr};" so the function uses the module-level
hickory_proto::{op, rr} imports instead, avoiding shadowing and redundancy while
leaving the rest of the test (name, op::Message::query(), etc.) unchanged.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 454b05d1-ba27-4727-85b3-9b483aa2c46e

📥 Commits

Reviewing files that changed from the base of the PR and between a1ca522 and 2be01fc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • clash-dns/Cargo.toml
  • clash-dns/src/handler.rs
  • clash-lib/Cargo.toml
  • clash-lib/src/app/dns/dns_client.rs
  • clash-lib/src/app/dns/resolver/enhanced.rs
  • clash-lib/src/app/dns/runtime.rs

Comment thread clash-dns/src/handler.rs
Comment on lines +557 to +559
let stream = stream_future.await.inspect_err(|e| {
assert!(false, "Failed to connect to DoT server: {}", e);
})?;

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.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Avoid assert!(false, …) inside inspect_err; let ? propagate the error.

Panicking inside inspect_err is unidiomatic — the trailing ? already fails the test with the error, so the assert is dead in the success path and just masks the propagation in the failure path. Drop the inspect_err (or replace the whole expression with .expect(...) if you want a tailored panic message).

♻️ Proposed simplification
-        let stream = stream_future.await.inspect_err(|e| {
-            assert!(false, "Failed to connect to DoT server: {}", e);
-        })?;
+        let stream = stream_future
+            .await
+            .map_err(|e| anyhow::anyhow!("Failed to connect to DoT server: {e}"))?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clash-dns/src/handler.rs` around lines 557 - 559, The current use of
inspect_err with assert!(false, ...) around stream_future.await is unidiomatic
and masks proper error propagation; remove the inspect_err call and simply await
the future with the trailing ? so the error propagates (i.e., assign stream =
stream_future.await?), or if you want a custom panic use .expect(...) on the
awaited result instead; update the expression that binds stream (where
stream_future.await is used and inspect_err is called) accordingly.

Comment on lines 858 to +868
#[tokio::test]
async fn test_bad_labels_with_custom_resolver() {
// Use hickory_client::proto (0.25) types throughout so DnsRequest::new
// gets a compatible Message. TODO: remove shadowing once hickory-client
// 0.26.0 stable ships and aligns with hickory-proto 0.26.
use hickory_client::proto::{op, rr};
use hickory_net::proto::{op, rr};

let name = rr::Name::from_str_relaxed("some_domain.understore")
.unwrap()
.append_domain(&rr::Name::root())
.unwrap();
assert_eq!(name.to_string(), "some_domain.understore.");

let mut m = op::Message::new();
let mut m = op::Message::query();

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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for a `proto` re-export inside hickory-net 0.26 by checking the published crate's lib.rs.
pip install --quiet requests >/dev/null 2>&1 || true
python - <<'PY'
import urllib.request, io, tarfile, sys
url = "https://crates.io/api/v1/crates/hickory-net/0.26.0/download"
data = urllib.request.urlopen(url).read()
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf:
    for m in tf.getmembers():
        if m.name.endswith("src/lib.rs"):
            f = tf.extractfile(m)
            src = f.read().decode("utf-8", errors="replace")
            print(m.name)
            for i, line in enumerate(src.splitlines(), 1):
                if "proto" in line and ("pub use" in line or "pub mod" in line or "extern crate" in line):
                    print(f"{i}: {line}")
            break
PY

Repository: Watfaq/clash-rs

Length of output: 2641


🏁 Script executed:

# Check Cargo.toml for hickory dependency versions
cat Cargo.toml | grep -A 10 "hickory"

Repository: Watfaq/clash-rs

Length of output: 41


🏁 Script executed:

# Read the actual import section and test function context from the file
sed -n '735,875p' clash-lib/src/app/dns/resolver/enhanced.rs

Repository: Watfaq/clash-rs

Length of output: 4929


🌐 Web query:

hickory-net 0.26 proto module re-export

💡 Result:

In hickory-net (including the 0.26 line), the crate re-exports the hickory-proto crate as proto. Evidence: the hickory-net crate docs show under “Re-exports” the item hickory_proto pub use as proto; [1].

Citations:


Remove the redundant use hickory_net::proto::{op, rr}; import inside the test function.

The module-level imports (lines 740-743) already bring hickory_proto::{op, rr} into scope. The inner import on line 860 redundantly re-imports the same items via hickory_net's re-export of hickory_proto as proto. While the code compiles successfully, the inner import should be removed to avoid unnecessary shadowing and improve clarity.

♻️ Proposed cleanup
     #[tokio::test]
     async fn test_bad_labels_with_custom_resolver() {
-        use hickory_net::proto::{op, rr};
-
         let name = rr::Name::from_str_relaxed("some_domain.understore")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[tokio::test]
async fn test_bad_labels_with_custom_resolver() {
// Use hickory_client::proto (0.25) types throughout so DnsRequest::new
// gets a compatible Message. TODO: remove shadowing once hickory-client
// 0.26.0 stable ships and aligns with hickory-proto 0.26.
use hickory_client::proto::{op, rr};
use hickory_net::proto::{op, rr};
let name = rr::Name::from_str_relaxed("some_domain.understore")
.unwrap()
.append_domain(&rr::Name::root())
.unwrap();
assert_eq!(name.to_string(), "some_domain.understore.");
let mut m = op::Message::new();
let mut m = op::Message::query();
#[tokio::test]
async fn test_bad_labels_with_custom_resolver() {
let name = rr::Name::from_str_relaxed("some_domain.understore")
.unwrap()
.append_domain(&rr::Name::root())
.unwrap();
assert_eq!(name.to_string(), "some_domain.understore.");
let mut m = op::Message::query();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clash-lib/src/app/dns/resolver/enhanced.rs` around lines 858 - 868, Remove
the redundant inner import inside the test_bad_labels_with_custom_resolver test:
delete the line "use hickory_net::proto::{op, rr};" so the function uses the
module-level hickory_proto::{op, rr} imports instead, avoiding shadowing and
redundancy while leaving the rest of the test (name, op::Message::query(), etc.)
unchanged.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@clash-dns/src/handler.rs`:
- Around line 35-41: The TLS CertifiedKey creation currently panics on missing
crypto provider or unsupported key; make this fallible by replacing the
expect(...) calls in the CertifiedKey::new argument construction (the calls to
rustls::crypto::CryptoProvider::get_default() and key_provider.load_private_key)
with checked logic that returns a propagated error (io::Error or DNSError) from
get_dns_listener instead of aborting; update the call site that constructs the
CertifiedKey to return Err(...) on failure and change the three listener
registration sites that currently use .into() to use .try_into()? so failures
bubble up.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4e91da45-9134-405f-bc25-25d64954badf

📥 Commits

Reviewing files that changed from the base of the PR and between 2be01fc and 9f55e98.

📒 Files selected for processing (3)
  • clash-dns/src/handler.rs
  • clash-lib/src/proxy/anytls/inbound/handler.rs
  • clash-lib/src/proxy/anytls/inbound/tls.rs

Comment thread clash-dns/src/handler.rs
Comment on lines +35 to +41
CertifiedKey::new(
pair.certs,
rustls::crypto::CryptoProvider::get_default()
.expect("no default crypto provider installed")
.key_provider
.load_private_key(pair.key)
.expect("unsupported private key type"),

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "handler.rs" -path "*/clash-dns/*"

Repository: Watfaq/clash-rs

Length of output: 86


🏁 Script executed:

head -100 clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 3708


🏁 Script executed:

sed -n '30,50p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 837


🏁 Script executed:

wc -l clash-dns/src/handler.rs

Repository: Watfaq/clash-rs

Length of output: 88


🏁 Script executed:

sed -n '1,200p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 7781


🏁 Script executed:

grep -n "get_dns_listener" clash-dns/src/handler.rs

Repository: Watfaq/clash-rs

Length of output: 185


🏁 Script executed:

grep -n "\.into()" clash-dns/src/handler.rs | head -20

Repository: Watfaq/clash-rs

Length of output: 521


🏁 Script executed:

grep -n "CertificateKeyPair" clash-dns/src/handler.rs

Repository: Watfaq/clash-rs

Length of output: 359


🏁 Script executed:

sed -n '203,230p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 1109


🏁 Script executed:

sed -n '260,280p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 932


🏁 Script executed:

sed -n '300,320p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 921


🏁 Script executed:

sed -n '340,360p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 953


🏁 Script executed:

sed -n '203,230p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 1109


🏁 Script executed:

sed -n '265,275p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 511


🏁 Script executed:

grep -A 3 "register_https_listener\|register_tls_listener\|register_h3_listener" clash-dns/src/handler.rs | head -30

Repository: Watfaq/clash-rs

Length of output: 525


🏁 Script executed:

sed -n '203,225p' clash-dns/src/handler.rs | cat -n

Repository: Watfaq/clash-rs

Length of output: 944


Avoid panicking on TLS provider/key setup in the listener path.

This runs in the production DoH/DoT/DoH3 startup path, so a missing default CryptoProvider or an unsupported key now aborts the process instead of returning a config error through get_dns_listener. Make this conversion fallible and propagate an io::Error/DNSError back to the caller.

♻️ Suggested direction
-impl From<CertificateKeyPair> for Arc<dyn rustls::server::ResolvesServerCert> {
-    fn from(pair: CertificateKeyPair) -> Self {
-        Arc::new(AlwaysResolvesServerRawPublicKeys::new(Arc::new(
-            CertifiedKey::new(
-                pair.certs,
-                rustls::crypto::CryptoProvider::get_default()
-                    .expect("no default crypto provider installed")
-                    .key_provider
-                    .load_private_key(pair.key)
-                    .expect("unsupported private key type"),
-            ),
-        )))
-    }
-}
+impl TryFrom<CertificateKeyPair> for Arc<dyn rustls::server::ResolvesServerCert> {
+    type Error = std::io::Error;
+
+    fn try_from(pair: CertificateKeyPair) -> Result<Self, Self::Error> {
+        let provider = rustls::crypto::CryptoProvider::get_default()
+            .ok_or_else(|| std::io::Error::other("no default crypto provider installed"))?;
+        let key = provider
+            .key_provider
+            .load_private_key(pair.key)
+            .map_err(|e| {
+                std::io::Error::new(
+                    std::io::ErrorKind::InvalidInput,
+                    format!("unsupported private key type: {e}"),
+                )
+            })?;
+
+        Ok(Arc::new(AlwaysResolvesServerRawPublicKeys::new(Arc::new(
+            CertifiedKey::new(pair.certs, key),
+        ))))
+    }
+}

Then switch the three registration sites from .into() to .try_into()?.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clash-dns/src/handler.rs` around lines 35 - 41, The TLS CertifiedKey creation
currently panics on missing crypto provider or unsupported key; make this
fallible by replacing the expect(...) calls in the CertifiedKey::new argument
construction (the calls to rustls::crypto::CryptoProvider::get_default() and
key_provider.load_private_key) with checked logic that returns a propagated
error (io::Error or DNSError) from get_dns_listener instead of aborting; update
the call site that constructs the CertifiedKey to return Err(...) on failure and
change the three listener registration sites that currently use .into() to use
.try_into()? so failures bubble up.

ibigbug and others added 2 commits May 11, 2026 13:05
- clash-dns/src/handler.rs: move TokioRuntimeProvider import from
  hickory_proto::runtime to hickory_net::runtime (it was never in proto);
  fix A record comparison: A has no ::new() in 0.26, use ip.0 == Ipv4Addr
- clash-lib/src/app/dns/resolver/enhanced.rs: replace removed
  Message::set_recursion_desired() with direct field m.metadata.recursion_desired;
  replace Client::connect().await (removed) with Client::from_sender() (sync);
  replace DnsRequest::set_id() (removed) with req.metadata.id = ...

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- handler.rs: H3ClientStreamBuilder::builder() → H3ClientStream::builder()
  (builder() is a static method on H3ClientStream, not H3ClientStreamBuilder);
  import H3ClientStream alongside H3ClientStreamBuilder
- handler.rs: response.answers() → &response.answers (now a pub field in 0.26.1)
- handler.rs: answers[0].data() → &answers[0].data (pub field; method requires
  RecordData trait in scope, field access is simpler)
- enhanced.rs: bring DnsHandle and FirstAnswer traits into test scope so
  client.send() and .first_answer() resolve correctly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@clash-lib/src/app/dns/resolver/enhanced.rs`:
- Around line 858-887: The test test_bad_labels_with_custom_resolver currently
depends on the public resolver at 1.1.1.1:53 which makes it flaky in CI; either
run it against a local DNS test server or exclude it from default runs—update
the test declaration (fn test_bad_labels_with_custom_resolver) to use a local
mock resolver (replace UdpClientStream/DnsRuntimeProvider setup with a local
test server equivalent) or simply add the #[ignore] attribute so CI won't run it
by default; ensure any spawned background client (client::Client::from_sender /
tokio::spawn(bg)) still compiles with the chosen approach.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 66b041d4-5440-4163-98ee-ecd58829eb91

📥 Commits

Reviewing files that changed from the base of the PR and between 9f55e98 and 80043ab.

📒 Files selected for processing (2)
  • clash-dns/src/handler.rs
  • clash-lib/src/app/dns/resolver/enhanced.rs

Comment on lines 858 to 887
#[tokio::test]
async fn test_bad_labels_with_custom_resolver() {
// Use hickory_client::proto (0.25) types throughout so DnsRequest::new
// gets a compatible Message. TODO: remove shadowing once hickory-client
// 0.26.0 stable ships and aligns with hickory-proto 0.26.
use hickory_client::proto::{op, rr};
use hickory_net::proto::{op, rr};

let name = rr::Name::from_str_relaxed("some_domain.understore")
.unwrap()
.append_domain(&rr::Name::root())
.unwrap();
assert_eq!(name.to_string(), "some_domain.understore.");

let mut m = op::Message::new();
let mut m = op::Message::query();
let mut q = op::Query::new();

q.set_name(name);
q.set_query_type(rr::RecordType::A);
m.add_query(q);
m.set_recursion_desired(true);
m.metadata.recursion_desired = true;

let stream = UdpClientStream::builder(
"1.1.1.1:53".parse().unwrap(),
DnsRuntimeProvider::new_direct(None, None),
)
.build();
let (client, bg) = client::Client::connect(stream).await.unwrap();

let (client, bg) = client::Client::<DnsRuntimeProvider>::from_sender(stream);
tokio::spawn(bg);

let mut req = DnsRequest::new(m, DnsRequestOptions::default());
req.set_id(rand::random::<u16>());
req.metadata.id = rand::random::<u16>();
let res = client.send(req).first_answer().await;
assert!(res.is_ok());

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep this live-network test out of the default suite.

This still depends on the public resolver at 1.1.1.1:53, so it can fail in offline or rate-limited CI while the other network-dependent tests in this module are already ignored. Either back it with a local test server or mark it ignored as well.

♻️ Minimal containment
     #[tokio::test]
+    #[ignore = "uses public DNS and is flaky in CI"]
     async fn test_bad_labels_with_custom_resolver() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[tokio::test]
async fn test_bad_labels_with_custom_resolver() {
// Use hickory_client::proto (0.25) types throughout so DnsRequest::new
// gets a compatible Message. TODO: remove shadowing once hickory-client
// 0.26.0 stable ships and aligns with hickory-proto 0.26.
use hickory_client::proto::{op, rr};
use hickory_net::proto::{op, rr};
let name = rr::Name::from_str_relaxed("some_domain.understore")
.unwrap()
.append_domain(&rr::Name::root())
.unwrap();
assert_eq!(name.to_string(), "some_domain.understore.");
let mut m = op::Message::new();
let mut m = op::Message::query();
let mut q = op::Query::new();
q.set_name(name);
q.set_query_type(rr::RecordType::A);
m.add_query(q);
m.set_recursion_desired(true);
m.metadata.recursion_desired = true;
let stream = UdpClientStream::builder(
"1.1.1.1:53".parse().unwrap(),
DnsRuntimeProvider::new_direct(None, None),
)
.build();
let (client, bg) = client::Client::connect(stream).await.unwrap();
let (client, bg) = client::Client::<DnsRuntimeProvider>::from_sender(stream);
tokio::spawn(bg);
let mut req = DnsRequest::new(m, DnsRequestOptions::default());
req.set_id(rand::random::<u16>());
req.metadata.id = rand::random::<u16>();
let res = client.send(req).first_answer().await;
assert!(res.is_ok());
#[tokio::test]
#[ignore = "uses public DNS and is flaky in CI"]
async fn test_bad_labels_with_custom_resolver() {
use hickory_net::proto::{op, rr};
let name = rr::Name::from_str_relaxed("some_domain.understore")
.unwrap()
.append_domain(&rr::Name::root())
.unwrap();
assert_eq!(name.to_string(), "some_domain.understore.");
let mut m = op::Message::query();
let mut q = op::Query::new();
q.set_name(name);
q.set_query_type(rr::RecordType::A);
m.add_query(q);
m.metadata.recursion_desired = true;
let stream = UdpClientStream::builder(
"1.1.1.1:53".parse().unwrap(),
DnsRuntimeProvider::new_direct(None, None),
)
.build();
let (client, bg) = client::Client::<DnsRuntimeProvider>::from_sender(stream);
tokio::spawn(bg);
let mut req = DnsRequest::new(m, DnsRequestOptions::default());
req.metadata.id = rand::random::<u16>();
let res = client.send(req).first_answer().await;
assert!(res.is_ok());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clash-lib/src/app/dns/resolver/enhanced.rs` around lines 858 - 887, The test
test_bad_labels_with_custom_resolver currently depends on the public resolver at
1.1.1.1:53 which makes it flaky in CI; either run it against a local DNS test
server or exclude it from default runs—update the test declaration (fn
test_bad_labels_with_custom_resolver) to use a local mock resolver (replace
UdpClientStream/DnsRuntimeProvider setup with a local test server equivalent) or
simply add the #[ignore] attribute so CI won't run it by default; ensure any
spawned background client (client::Client::from_sender / tokio::spawn(bg)) still
compiles with the chosen approach.

@coderabbitai coderabbitai Bot left a comment

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.

♻️ Duplicate comments (1)
clash-dns/src/handler.rs (1)

32-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Panicking From impl is still on the listener startup path.

This conversion is invoked by register_https_listener, register_tls_listener, and register_h3_listener (lines 269, 310, 350) via .into(), so a missing default CryptoProvider or an unsupported key now aborts the process at server start rather than surfacing as a DNSError from get_dns_listener. Recommend converting this to TryFrom and switching the three call sites to .try_into()? so failures propagate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@clash-dns/src/handler.rs` around lines 32 - 45, Replace the infallible
From<CertificateKeyPair> implementation with a TryFrom<CertificateKeyPair> that
returns Result<Arc<dyn rustls::server::ResolvesServerCert>, DNSError> and
propagate failures from CryptoProvider::get_default() and
key_provider.load_private_key(...) instead of calling expect; construct and
return the Arc via
AlwaysResolvesServerRawPublicKeys::new(CertifiedKey::new(...)) on success. Then
update the three call sites register_https_listener, register_tls_listener, and
register_h3_listener to use .try_into()? (or handle the Result and map to
DNSError) so missing crypto provider or unsupported key types surface as
DNSError from get_dns_listener rather than panicking at startup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@clash-dns/src/handler.rs`:
- Around line 32-45: Replace the infallible From<CertificateKeyPair>
implementation with a TryFrom<CertificateKeyPair> that returns Result<Arc<dyn
rustls::server::ResolvesServerCert>, DNSError> and propagate failures from
CryptoProvider::get_default() and key_provider.load_private_key(...) instead of
calling expect; construct and return the Arc via
AlwaysResolvesServerRawPublicKeys::new(CertifiedKey::new(...)) on success. Then
update the three call sites register_https_listener, register_tls_listener, and
register_h3_listener to use .try_into()? (or handle the Result and map to
DNSError) so missing crypto provider or unsupported key types surface as
DNSError from get_dns_listener rather than panicking at startup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 130ff7c6-5fa6-4552-b788-f685c7eb02ce

📥 Commits

Reviewing files that changed from the base of the PR and between 80043ab and 4a72eee.

📒 Files selected for processing (2)
  • clash-dns/src/handler.rs
  • clash-lib/src/app/dns/resolver/enhanced.rs

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.72727% with 21 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
clash-lib/src/app/dns/dns_client.rs 40.00% 12 Missing and 3 partials ⚠️
clash-dns/src/handler.rs 85.71% 2 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@ibigbug ibigbug merged commit 57a0a1a into master May 11, 2026
33 checks passed
@ibigbug ibigbug deleted the sunset-hickory-client branch May 11, 2026 21:00
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.

1 participant