本次审阅在 wind/tuic Rust 工作区的 13 个子系统中共确认 99 项问题。整体而言,核心协议层(proto)与 Quinn 服务端/客户端实现存在多处高危正确性与并发缺陷:QUIC 单向流未调用 finish() 导致接收端永远看不到 EOF、UDP 会话缓存驱逐不释放任务(资源泄漏)、ALPN 硬编码 "h3" 忽略调用者配置、Bytes::copy_to_bytes 不校验长度可被远端 panic、tuic-server 旧版字段通过 #[serde(rename)] 互换导致 QUIC 窗口参数静默错配等。安全默认值亦有重大风险:默认配置关闭 TLS 证书校验、0-RTT 启用但应用层无重放保护、SOCKS5 UDP 中继与 tuic-client 的 UDP 关联均未校验源地址,任何能预测端口的攻击者均可劫持会话。Naive/SOCKS 子系统的 UDP 路径存在 IPv4-only 套接字、按包 spawn、未限速无界 channel、SOCKS5 回复头使用错误地址等多重协议/性能问题。规则引擎与调度器中存在多处影响面广的正确性偏差:rule_target_to_action 将出站名小写化造成路由错配、UDP 调度统一用 0.0.0.0:0 哨兵让所有 UDP ACL 失效、空 AND/OR 形成"恒真/恒假"悖论。建议修复顺序:(1) 安全默认值与认证/0-RTT/源校验;(2) 协议/编解码 panic 与流终止;(3) 会话/任务泄漏与并发控制;(4) 路由/规则/配置正确性;(5) 性能与代码质量清理。
- 总确认问题数: 99
- 按严重度: critical 0, high 26, medium 35, low 27, nit 11
- 按类别: correctness 53, concurrency 10, security 14, performance 9, quality 13
- 按子系统:
- proto-core: 6
- proto-udp-stream: 7
- quinn-inbound: 6
- quinn-rest: 13
- core-rule: 9
- core-dispatcher-net: 4
- core-misc: 5
- socks-base-naive: 18
- dns-acme-binary: 7
- tuic-server-config: 4
- tuic-server-acl: 5
- tuic-server-rest: 8
- tuic-client: 11
(无)
- 位置:
crates/wind-tuic/src/proto/mod.rs:161-163, 206-208, 252-254, 258-264 - 类别: correctness / 置信度: high
- 描述:所有单向流帮助函数(
encode_and_send_uni、send_auth、UDP 非 datagram 分支、drop_udp)调用了write_chunk/write_all_chunks后从不调用send.finish()。Quinn 在finish()被调用或SendStream被 drop 之前不会刷新流结束信号——而未 finish 的 SendStream 被 drop 时会 reset 流。接收端通常以read_to_end读取直至 EOF,因此要么挂起,要么看到流 reset 而非干净结束。这在实际环境下破坏了 auth/dissociate/uni-udp 路径。 - 建议:在返回前调用
send.finish()?(必要时send.stopped().await?),让对端看到干净的流结束而非 drop 时的 reset。 - 证据:
let mut send = conn.open_uni().await?;
send.write_chunk(buf.into()).await?;
Ok(())- 位置:
crates/wind-tuic/src/quinn/inbound.rs:726-793 - 类别: concurrency / 置信度: high
- 描述:
get_or_create_session中tuic_stream: Arc<UdpStream>同时被 spawn 的两个桥接任务捕获,缓存(容量u16::MAX)驱逐条目时只丢弃自己的 Arc,任务持有的 Arc 仍存活;from_outbound_rx/to_outbound_tx循环唯有在 channel 对端关闭时才退出,而 channel 对端正是被任务自身持有的对象——形成持有环,任务永生。客户端循环切换 assoc_id 可让单连接累积无界内存。 - 建议:为每个 session 引入
CancellationToken(连接 token 的子 token),handle_dissociate和缓存eviction_listener中触发 cancel,桥接任务tokio::select!在 token 上感知退出。 - 证据:
let response_stream = tuic_stream.clone();
tokio::spawn(async move {
while let Some(packet) = from_outbound_rx.recv().await {
if let Err(e) = response_stream.send_packet(packet).await { ... break; }
}
});- 位置:
crates/wind-tuic/src/quinn/tls.rs:27 - 类别: security / 置信度: high
- 描述:客户端 TLS ALPN 被硬编码为
["h3"],无视调用方提供的opts.alpn。TUIC 服务通常协商"tuic",只播报"h3"会与正确配置的对端发生 ALPN 不匹配,而静默忽略用户配置也会迫使客户端连上 HTTP/3 端点,弱化安全态势。 - 建议:
opts.alpn.iter().map(|a| a.as_bytes().to_vec()).collect(),仅在列表为空时回退到合理默认(例如"tuic")。 - 证据:
config.alpn_protocols = vec![String::from("h3")].into_iter().map(|alpn| alpn.into_bytes()).collect();- 位置:
crates/wind-tuic/src/quinn/outbound.rs:180 - 类别: correctness / 置信度: high
- 描述:
size是从网络直接解析的 16 位负载长度,Bytes::copy_to_bytes在剩余字节不足时会 panic。恶意对端只需在 Packet 命令中声明size大于实际剩余字节,即可使出站任务(进而 spawn 路径 panic)崩溃。 - 建议:在调用
copy_to_bytes前校验size as usize <= buf.remaining(),否则返回解码错误。 - 证据:
let payload = buf.copy_to_bytes(size as usize);- 位置:
crates/wind-tuic/src/quiche/inbound.rs:75-108 - 类别: correctness / 置信度: high
- 描述:接受循环只读取 UDP 包并日志输出;没有
quiche::accept、conn.recv、conn.send、没有连接表、无定时器处理。listen静默返回但根本不提供服务,self.users也未消费。客户端会无诊断地超时。 - 建议:要么实现完整状态机,要么从
listen返回明确错误并将其特性门置为unimplemented而非server。 - 证据:
warn!("Received QUIC packet from {}, DCID len: {}", src, hdr.dcid.len());- 位置:
crates/wind-core/src/rule.rs:833-852 - 类别: performance / 置信度: high
- 描述:对
DomainWildcard规则的每次匹配都从头编译正则,并重新小写输入。正则编译开销大且发生在每连接的路由热路径上。应在解析时预编译并存进RuleType变体。 - 建议:在
parse_type处理 DOMAIN-WILDCARD 分支时即将通配符转换为Regex,在DomainWildcard中存储Regex。 - 证据:
let mut re = String::from("^");
for ch in pattern.chars() {
match ch { '*' => re.push_str(".*"), ... }
}
Regex::new(&re).is_ok_and(|r| r.is_match(&text))- 位置:
crates/wind-core/src/dispatcher.rs:294-298 - 类别: correctness / 置信度: high
- 描述:在匹配 reject/block/deny 后,fallback 分支以
name.to_string()转发,而name已被绑定为小写串。Dispatcher::resolve_handler的 HashMap 查找是区分大小写的,因此Proxy_Out被静默小写后落入 default,无错误。 - 建议:只为 reject 关键字匹配做小写,转发时使用原始
target字符串。 - 证据:
match target.to_ascii_lowercase().as_str() {
"reject" | "block" | "deny" => RouteAction::Reject(format!("rejected by rule: {}", rule)),
name => RouteAction::Forward(name.to_string()),
}- 位置:
crates/wind-core/src/dispatcher.rs:187-206 - 类别: correctness / 置信度: high
- 描述:
dispatch_udp构造TargetAddr::IPv4(0.0.0.0, 0)哨兵传入router.route(...),导致路由器看不到任何真实目的,所有 UDP 会话被同样路由,IP-CIDR/DOMAIN-SUFFIX/DST-PORT 等规则对 UDP 完全失效。 - 建议:将
Router::route改为对 UDP 接受Option<&TargetAddr>并在看到首个包后路由,或新增route_udpAPI。 - 证据:
let sentinel = TargetAddr::IPv4(std::net::Ipv4Addr::UNSPECIFIED, 0);
let action = self.router.route(&sentinel, false).await?;- 位置:
crates/wind-core/src/io.rs:18-55 - 类别: correctness / 置信度: high
- 描述:
select!循环遇到 EOF(num == 0)立即 break 返回。TCP/HTTP/QUIC 隧道经常先半关闭一方向(例如 HTTP 客户端在请求后发 FIN),代理需要继续转发另一方向直至其也 EOF。当前实现丢数据并提前终止连接。 - 建议:改用
tokio::io::copy_bidirectional,或在 EOF 时对反向 writer 调用shutdown()并继续转发剩余方向。 - 证据:
Ok(num) => {
// EOF
if num == 0 {
break;
}- 位置:
crates/wind-socks/src/inbound.rs:117 - 类别: correctness / 置信度: high
- 描述:
SocksInboundOpt::public_addr文档明确为"用于 UDP 回复",但 UDPAssociate 回复 IP 被硬编码为127.0.0.1,远端客户端会向自己的环回发送 UDP,导致中继永远收不到。 - 建议:
allow_udp启用时使用opts.public_addr.ok_or(...)?(或回退到 listener 本地地址);若未设置则在配置层拒绝。 - 证据:
let reply_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
crate::ext::run_udp_proxy(proto, &target_addr, None, reply_ip, move |inbound| async move {- 位置:
crates/wind-socks/src/udp.rs:142-147 - 类别: correctness / 置信度: high
- 描述:RFC 1928 §7 规定 UDP 回复中的 ATYP/DST.ADDR/DST.PORT 必须标识远端来源主机;此处
new_udp_header(current_client)将客户端自身地址写入回复源,客户端永远只看到自己的地址,无法对多个远端进行多路复用解复用。 - 建议:回复头来自
packet.source(或packet.target),current_client仅作为send_to目的。 - 证据:
if let Ok(mut packet_with_header) = new_udp_header(current_client) {
packet_with_header.extend_from_slice(&packet.payload);
if let Err(e) = socket.send_to(&packet_with_header, current_client).await {- 位置:
crates/wind-socks/src/udp.rs:97-108 - 类别: security / 置信度: high
- 描述:
source_addr初始化为未指定地址,被最近一个发包者覆盖。任何能猜到中继ip:port的链外攻击者只需发一个 UDP 包即可劫持关联,合法上游回复将被转发给攻击者。RFC 1928 §6 明确要求丢弃源不匹配的报文。 - 建议:从 TCP 控制连接固定预期客户端地址(或首个观察到的对端),其他源一律丢弃。
- 证据:
let source_addr = Arc::new(ArcSwap::new(Arc::new(SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0))));
...
Ok((len, addr)) => {
source_addr_rx.store(Arc::new(addr));- 位置:
crates/wind-base/src/direct.rs:89 - 类别: correctness / 置信度: high
- 描述:
UdpSocket::bind("0.0.0.0:0")仅创建 IPv4 socket。当resolve_target返回 IPv6 时send_to失败EAFNOSUPPORT,所有 IPv6 UDP 流量静默损坏。 - 建议:绑定双栈
[::]:0(并禁用IPV6_V6ONLY)或按目标族选择套接字。 - 证据:
let relay_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?);- 位置:
crates/wind-naive/src/lib.rs:199-223 - 类别: concurrency / 置信度: high
- 描述:每收到一个 UDP 包就 spawn 一个分离任务并新开一条 CONNECT 隧道写入再丢弃。(1) spawn 无并发上限,UDP 洪水即 DoS;(2) 每包付完整 TLS+HTTP 握手代价,等同不可用;(3)
tx(响应)被命名为_tx并丢弃,UDP 回复永不送回——本质单向。 - 建议:按 Naive 协议实现 UDP-over-TLS 多路复用(每远端或单流多路复用),限制并发 spawn,且将
tx接通至应答传递。或暂时移除该实现并返回错误。 - 证据:
let UdpStream { tx: _tx, mut rx } = udp_stream;
...
tokio::spawn(async move {
if let Err(e) = udp_tunnel_tx(client, &target_str, &payload).await {- 位置:
crates/wind/src/conf/persistent.rs:65, 182 - 类别: security / 置信度: high
- 描述:默认
PersistentConfig与skip_cert_verify的 serde 默认值均为 true。运行wind init或依赖默认值的用户得到的 TUIC 出站接受任意证书,完全失去 TLS 意义,允许对上游中继的轻易 MITM。 - 建议:将默认改为 false,关闭校验须显式 opt-in(且应在日志中大声警告)。
- 证据:
skip_cert_verify: true, ...
#[serde(default = "default_true")]
pub skip_cert_verify: bool,- 位置:
crates/tuic-server/src/config.rs:182-187 - 类别: correctness / 置信度: high
- 描述:
__send_window被声明为Option<u64>但rename = "receive_window",反之亦然。用户提供的旧顶层send_window实际反序列化到__receive_window,migrate()再写入quic.send_window——结果旧send_window变成新quic.receive_window,QUIC 流控参数被静默互换。 - 建议:交换
rename字符串(或字段名),使__send_windowrename 为"send_window"。 - 证据:
#[serde(default, rename = "receive_window")]
#[deprecated]
pub __send_window: Option<u64>,
#[serde(default, rename = "send_window")]
#[deprecated]
pub __receive_window: Option<u32>,- 位置:
crates/tuic-server/src/config.rs:610-613 - 类别: correctness / 置信度: high
- 描述:
A && B && C || D解析为(A && B && C) || D,任何非空、非注释、含=的行(如 YAML 值secret: aGVsbG8=)都被识别为 TOML;tie-break 又偏 TOML——YAML 文件被当作 TOML 解析失败。 - 建议:显式加括号,且把
=检测限制到^[A-Za-z_][\w-]*\s*=这样的键值形态。 - 证据:
let has_toml_patterns = lines.iter().any(|line| {
let trimmed_line = line.trim();
trimmed_line.starts_with('[') && trimmed_line.contains(']') && !trimmed_line.contains(':') || trimmed_line.contains('=')
});- 位置:
crates/tuic-server/src/acl.pest:34-39, 51-56 - 类别: correctness / 置信度: high
- 描述:
ASCII_DIGIT{1,3}接受到 999,CIDR 前缀也无范围限制。parse_address_from_pair仅存原文,运行时要么不匹配,要么落到address_to_rule_types的0.0.0.0/32兜底,掩盖了用户的配置错误。 - 建议:在文法层收紧到 0-255 与有效前缀,或在
parse_address_from_pair中以Ipv4Addr/IpNetwork校验。 - 证据:
ipv4 = @{
ASCII_DIGIT{1,3} ~ "." ~
ASCII_DIGIT{1,3} ~ "." ~
ASCII_DIGIT{1,3} ~ "." ~
ASCII_DIGIT{1,3}
}- 位置:
crates/tuic-server/src/tls.rs:46-58 - 类别: correctness / 置信度: high
- 描述:
start_watch返回Result<()>并由tokio::spawnawait;calc_hash任意瞬时错误(ACME 续期时 rename 中途的 ENOENT、权限抖动)直接传播退出循环,此后热重载永久失效。 - 建议:在循环内 match
calc_hash结果,出错只记日志并continue,不要从 watcher 任务中传播错误。 - 证据:
let hash = Self::calc_hash(&self.cert_path, &self.key_path).await?;
if &hash != self.hash.swap(hash.into()).deref() {
match self.reload_cert_key().await {- 位置:
crates/tuic-server/src/tls.rs:50-56 - 类别: correctness / 置信度: high
- 描述:
self.hash.swap(...)无条件存入新哈希后才执行reload_cert_key;若 reload 失败(写入中途、不支持的 key),新哈希已写入,下次轮询认为"未变化"不再重试,旧(可能过期)证书被一直使用且无后续警告。 - 建议:先
load()对比,仅在reload_cert_key成功后swap新哈希。 - 证据:
if &hash != self.hash.swap(hash.into()).deref() {
match self.reload_cert_key().await {
Ok(_) => warn!("Successfully reloaded TLS certificate and key"),
Err(e) => warn!("Failed to reload TLS certificate and key: {e}"),- 位置:
crates/tuic-client/src/lib.rs:26-37 - 类别: correctness / 置信度: high
- 描述:
forward::start()返回()并只 spawn 监听任务,bind 失败仅在被吞任务中warn!;之后socks5::Server::start()永久循环。主线程无法感知启动错误,错误配置的 forwarder 静默消失。 - 建议:
start()返回Result,在主线程使用JoinSet监控所有 forwarder。 - 证据:
forward::start(cfg.local.tcp_forward.clone(), cfg.local.udp_forward.clone()).await;
match socks5::Server::set_config(cfg.local) {- 位置:
crates/tuic-client/src/forward.rs:147-235 - 类别: correctness / 置信度: high
- 描述:每收到一个 UDP 包就 spawn 一个新任务建立一次性
UdpStream,发送后立即 drop,无法维持 5 元组状态,NAT 语义被破坏;每包还伴随 spawn 和 mpsc 创建。 - 建议:每
src_addr维持一条出站UdpStream,在 recv 循环中喂入;reply-bridge 每会话仅 spawn 一次。 - 证据:
tokio::spawn(async move {
let (tx_to_out, rx_from_local) = tokio::sync::mpsc::channel::<UdpPacket>(8);
let (tx_to_local, mut rx_from_out) = tokio::sync::mpsc::channel::<UdpPacket>(8);
let _ = tx_to_out.send(UdpPacket { source: None, target: target.clone(), payload: pkt }).await;
drop(tx_to_out);- 位置:
crates/tuic-client/src/socks5/udp_session.rs:139-141 - 类别: security / 置信度: high
- 描述:首个 datagram 触发
self.socket.connect(src_addr).await永久绑定对端。任何能在合法客户端之前发送 UDP 至中继端口的攻击者即可独占中继,无需认证。 - 建议:在 connect 前用 ACL 控制连接的
ctrl_addr.ip()校验src_addr.ip()。 - 证据:
if let Ok(connected_addr) = self.socket.peer_addr() {
...
} else {
self.socket.connect(src_addr).await?;
}- 位置:
crates/wind-tuic/src/proto/mod.rs:248-251 - 类别: performance / 置信度: high
- 描述:
Chain后立刻copy_to_bytes(combined.remaining())又分配又 memcpy,正好抵消了 Chain 的初衷,UDP 热路径上每包多一次拷贝。 - 建议:
BytesMut::with_capacity(12 + payload.len())预分配再extend_from_slice,然后self.send_datagram(buf.freeze())?。 - 证据:
if datagram {
let mut combined = buf.freeze().chain(payload);
self.send_datagram(combined.copy_to_bytes(combined.remaining()))?;
}- 位置:
crates/wind-tuic/src/proto/udp_stream.rs:291-293 - 类别: correctness / 置信度: high
- 描述:
load后再fetch_add不原子,两个并发send_packet可能用同一 pkt_id 发出两条 datagram,与接收端的分片重组状态发生冲突。 - 建议:替换为
let pkt_id = self.next_pkt_id.fetch_add(1, Ordering::Relaxed);,并使用返回值。 - 证据:
self.connection.send_udp(self.assoc_id, self.next_pkt_id.load(Ordering::Relaxed), ...).await?;
self.next_pkt_id.fetch_add(1, Ordering::Relaxed);- 位置:
crates/wind-tuic/src/proto/udp_stream.rs:92-131 - 类别: security / 置信度: medium
- 描述:
add_fragment直接信任 wire 上的frag_total/frag_id,既不校验frag_total > 0,也不校验frag_id < frag_total,后续片段也不校验frag_total一致;可触发空包立即"重组"、永久污染子缓存或者强占巨大缓存。 - 建议:拒绝
frag_total == 0、frag_id >= frag_total、frag_total与首片不一致的片。 - 证据:
fragments: Cache::new(frag_total.into()),
...
meta.value().fragments.insert(frag_id, payload).await;- 位置:
crates/wind-tuic/src/proto/udp_stream.rs:266-281 - 类别: correctness / 置信度: medium
- 描述:小
max_datagram_size时subsequent_frag_max_payload可为 0,div_ceil(0)panic;敌意域名也能让首片头开销膨胀触发同问题。 - 建议:若任一最大负载为 0,提前返回错误。
- 证据:
let first_frag_max_payload = max_datagram_size.saturating_sub(first_frag_header_overhead);
let subsequent_frag_max_payload = max_datagram_size.saturating_sub(subsequent_frag_header_overhead);
1 + remaining.div_ceil(subsequent_frag_max_payload)- 位置:
crates/wind-tuic/src/proto/udp_stream.rs:269-270, 348-352 - 类别: performance / 置信度: high
- 描述:每分片每发送都
info!分配格式化字符串并 I/O,远比包本身昂贵。 - 建议:降级为
debug!/trace!,info!仅用于状态变化。 - 证据:
wind_core::info!(target: "[UDP]", "Fragmentation params: payload={}, ...");
wind_core::info!(target: "[UDP]", "Sending fragment {}/{}: {} bytes", frag_id + 1, frag_total, datagram_size);- 位置:
crates/wind-tuic/src/proto/udp_stream.rs:147-153 - 类别: quality / 置信度: high
- 描述:
invalidate_entries_if失败实为静态构造期错误;warn 并继续会掩盖编程缺陷。 - 建议:构造时显式开启
support_invalidation_closures并在此expect。 - 证据:
if let Err(e) = self.fragments.invalidate_entries_if(move |_, meta| { ... }) {
wind_core::warn!(target: "[UDP]", "Failed to register fragment cleanup predicate: {:?}", e);
return;
}- 位置:
crates/wind-tuic/src/quinn/inbound.rs:334 - 类别: security / 置信度: medium
- 描述:容量 65535 等于整个 16 位 assoc_id 空间,每新 id spawn 三个任务并分配多个 channel,认证后的对端可在单连接上耗光资源。
- 建议:容量收紧到可配置较低值(256-1024),LRU/TTI 策略并在驱逐时取消任务。
- 证据:
udp_sessions: Cache::new(u16::MAX.into()),- 位置:
crates/wind-tuic/src/quinn/inbound.rs:242-264 - 类别: correctness / 置信度: medium
- 描述:可拒绝模式且无
else =>分支,Endpoint 正常关闭时select!全部分支被禁用,panic。 - 建议:增加
else => break,,或用match ... { Some(i) => ..., None => break }。 - 证据:
tokio::select! {
_ = self.cancel.cancelled() => { ... break; }
Some(incoming) = endpoint.accept() => { ... }
}- 位置:
crates/wind-tuic/src/quinn/inbound.rs:199-202 - 类别: security / 置信度: medium
- 描述:
max_early_data_size = u32::MAX且send_half_rtt_data = true,服务端接受所有 0-RTT 应用数据,无 nonce/计数器/幂等;攻击者可重放 Connect/Packet。 - 建议:0-RTT 仅允许 Heartbeat,或为 0-RTT 处理引入重放缓存/nonce 校验。
- 证据:
if self.opts.zero_rtt {
crypto.max_early_data_size = u32::MAX;
crypto.send_half_rtt_data = true;
}- 位置:
crates/wind-tuic/src/quinn/outbound.rs:255 - 类别: correctness / 置信度: medium
- 描述:
u16计数器 fetch_add Relaxed 回绕,重用仍存活的 id 会让insert静默覆盖原 Arc。 - 建议:探测未用 id 或显式上限,满时拒绝新会话。
- 证据:
let assoc_id = self.udp_assoc_counter.fetch_add(1, Ordering::SeqCst);
self.udp_session.insert(assoc_id, tuic_stream.clone()).await;- 位置:
crates/wind-tuic/src/quinn/task.rs:51-54 - 类别: concurrency / 置信度: high
- 描述:
send_timeout(_, 1s)超时即break,慢消费者杀死整个连接的接收。 - 建议:区分接收方 drop 与超时;超时应应用反压(丢最旧、记录并继续)而非退出循环。
- 证据:
if let Err(e) = tx.send_timeout(item, Duration::from_secs(1)).await {
warn!("{} channel send failed (receiver dropped or timeout): {e:?}", name);
break;
}- 位置:
crates/wind-tuic/src/quiche/outbound.rs:10-123 - 类别: correctness / 置信度: high
- 描述:仅存配置和 builder,无 connect、无
AbstractOutbound实现、无网络。client特性下发布会产生无用 handle。 - 建议:实现连接逻辑,或先移除类型与
clientfeature。 - 证据:
#[allow(dead_code)]
pub struct TuicheOutbound { ... password: Vec<u8>, opts: ConnectionOpts }- 位置:
crates/wind-tuic/src/quiche/outbound.rs:32-114 - 类别: quality / 置信度: high
- 描述:fluent setter 暴露这些字段但
build()不带入,静默失效。 - 建议:删除 setter,或注入到
TuicheOutbound/ConnectionOpts。 - 证据:
Ok(TuicheOutbound { server_addr, server_name, uuid, password: password.into_bytes(), opts: self.opts })- 位置:
crates/wind-tuic/src/quinn/outbound.rs:100-112 - 类别: correctness / 置信度: high
- 描述:
new不调用start_poll,忘记调用则心跳与入站不工作;重复调用会 spawn 重复 handler。 - 建议:在
new内自动调用,或用AtomicBool守卫幂等性。 - 证据:
pub async fn start_poll(&self) -> eyre::Result<()> { ... }- 位置:
crates/wind-core/src/rule.rs:319-321 - 类别: correctness / 置信度: high
- 描述:每次连接评估都分配并
format!,可预先小写并以字节切片比较。 - 建议:解析时预小写,字节级不分配比较。
- 证据:
h.eq_ignore_ascii_case(suffix) || h.to_ascii_lowercase().ends_with(&format!(".{}", suffix.to_ascii_lowercase()))- 位置:
crates/wind-core/src/rule.rs:323-325 - 类别: performance / 置信度: high
- 描述:同上,关键词应预先小写,使用无分配的 ASCII 不敏感子串搜索。
- 建议:存储小写 keyword,手动 ASCII 不敏感子串匹配。
- 证据:
RuleType::DomainKeyword(kw) => ctx.domain
.is_some_and(|h| h.to_ascii_lowercase().contains(&kw.to_ascii_lowercase())),- 位置:
crates/wind-core/src/rule.rs:608-624 - 类别: correctness / 置信度: high
- 描述:空 AND 永远为真(等同于 MATCH),空 OR 永远为假;
AND,(),DIRECT类配置直接成为 catch-all,危险。 - 建议:解析时拒绝空 AND/OR。
- 证据:
"AND" => { let sub = Self::parse_compound(value)?; Ok(RuleType::And(sub)) }
"OR" => { let sub = Self::parse_compound(value)?; Ok(RuleType::Or(sub)) }- 位置:
crates/wind-core/src/rule.rs:337-345 - 类别: correctness / 置信度: medium
- 描述:IpSuffix 也用
net.contains(&ip),与 IpCidr 完全等价。要么实现真正的后缀位语义,要么删掉。 - 建议:实现后缀位语义或移除变体。
- 证据:
RuleType::IpSuffix(net) => ctx.dst_ip.is_some_and(|ip| net.contains(&ip)),- 位置:
crates/wind-core/src/outbound.rs:9, 15 - 类别: quality / 置信度: high
- 描述:正因如此 dispatcher.rs 才有带
unreachable!()的NoChain。 - 建议:拆分两个方法或用
Option<&dyn AbstractOutbound>/Box<dyn AbstractOutbound>。 - 证据:
fn handle_tcp(
&self,
target_addr: TargetAddr,
stream: impl AbstractTcpStream,
via: Option<impl AbstractOutbound + Sized + Send>,
) -> impl Future<Output = eyre::Result<()>> + Send;- 位置:
crates/wind-core/src/interface.rs:44-89 - 类别: quality / 置信度: high
- 描述:
utils::StackPrefer已存在且更完善,interface::StackPrefer通过pub use被覆盖,但仍残留并带不同 serde 别名,易混淆。 - 建议:删除
interface::StackPrefer,统一使用utils::StackPrefer。 - 证据:
#[allow(dead_code)]
pub enum StackPrefer { V4, V6, V4V6, V6V4 }- 位置:
crates/wind-core/src/types.rs:82-100 - 类别: correctness / 置信度: high
- 描述:
":80"被解析为Domain("", 80)、"x x:80"被解析为Domain("x x", 80),后续 DNS 失败时报错不明显。 - 建议:用
rsplit_once(':'),校验 host 非空且至少做基本域名语法检查。 - 证据:
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 { return Err(Error::custom("Invalid address format, expected host:port")); }
Ok(TargetAddr::Domain(parts[0].to_string(), port))- 位置:
crates/wind-core/src/log.rs:37-39 - 类别: quality / 置信度: high
- 描述:
info!默认带target = crate-name,而warn!/error!无,过滤器配置失效。 - 建议:对
warn!/error!也补上target: $crate::extract_crate_name!()。 - 证据:
($($arg:tt)*) => {
$crate::log::tracing::warn!($($arg)*)
};- 位置:
crates/wind-base/src/direct.rs:93-132 - 类别: concurrency / 置信度: high
- 描述:
send_task/recv_task是分离tokio::spawn,select! 完成后另一任务仍持有 socket 与 channel 继续运行。 - 建议:用
JoinSet/abort_on_drop,或直接select!两个 future 不 spawn。 - 证据:
let send_task = tokio::spawn(async move { ... });
let recv_task = tokio::spawn(async move { ... });
tokio::select! { _ = send_task => {} _ = recv_task => {} }- 位置:
crates/wind-naive/src/lib.rs:284-317 - 类别: concurrency / 置信度: medium
- 描述:单线程先 drain 写、再阻塞读;上游静默时写无法发送。
- 建议:读写分线程,或将 socket 设非阻塞配合 eventfd/Waker。
- 证据:
if let Ok(data) = naive_write_rx.try_recv() {
if naive.write_all(&data).is_err() { return; }
let _ = naive.flush();
}
match naive.read(&mut read_buf) {- 位置:
crates/wind-naive/src/lib.rs:275-276 - 类别: concurrency / 置信度: medium
- 描述:
unbounded_channel无背压,慢消费者直接 OOM。 - 建议:有界
mpsc::channel(64)。 - 证据:
let (naive_write_tx, mut naive_write_rx) = mpsc::unbounded_channel::<Vec<u8>>();
let (naive_read_tx, mut naive_read_rx) = mpsc::unbounded_channel::<Vec<u8>>();- 位置:
crates/wind-naive/src/lib.rs:125-128 - 类别: correctness / 置信度: high
- 描述:
[2001:db8::1]:443被 split':'取首段得到"[2001",Cronet 收到无意义 SNI。 - 建议:用合适解析器或在 IPv6 字面量时强制要求
server_name。 - 证据:
.unwrap_or_else(|| opts.server_address.split(':').next().unwrap_or("").to_string());- 位置:
crates/wind/src/conf/persistent.rs:291-297 - 类别: correctness / 置信度: medium
- 描述:
foo.json被当成 TOML 解析;CLI 中宣称的BASE64-TEXT输入也未被处理。 - 建议:显式
.tomlvs.yaml/.yml,未知扩展返回错误;若要支持 base64 则先解码再 merge。 - 证据:
figment = if path.ends_with(".yaml") || path.ends_with(".yml") {
figment.merge(Yaml::file(path))
} else {
figment.merge(Toml::file(path))
};- 位置:
crates/wind-dns/src/config.rs:61-62 - 类别: correctness / 置信度: high
- 描述:
deny_unknown_fields加上没有默认值的可选字段在某些格式下会拒绝省略字段的配置。 - 建议:添加
#[serde(default)]。 - 证据:
/// Retry attempts per query. Defaults to the Hickory library default.
pub attempts: Option<usize>,- 位置:
crates/tuic-server/src/main.rs:37-53 - 类别: correctness / 置信度: high
- 描述:
AppContext::cancel仅被 spawn 任务作为父 token 取子 token,主线程 ctrl_c 后未cancel.cancel(),任务依赖 runtime 退出强制中止,且未 flush 日志守卫。 - 建议:从
run暴露/接收 cancel token,ctrl_c 后取消并 await server 任务。 - 证据:
res = tokio::signal::ctrl_c() => {
if let Err(err) = res { ... } else {
tracing::info!("Received Ctrl-C, shutting down.");
}
}- 位置:
crates/tuic-server/src/wind_adapter.rs:213-215 - 类别: quality / 置信度: high
- 描述:DoS 相关旋钮被硬编码,只能改源码。
- 建议:接入
Config。 - 证据:
max_concurrent_bi_streams: 512,
max_concurrent_uni_streams: 512,- 位置:
crates/tuic-server/src/acl.rs:627-636 - 类别: correctness / 置信度: high
- 描述:
2001:db8::1加/32解析成功并匹配整个2001:db8::/32,ACL 被静默扩大。 - 建议:先解析为
IpAddr再按 v4/v6 选 32/128。 - 证据:
if let Ok(net) = format!("{ip_str}/32").parse::<ipnet::IpNet>() {
vec![wrule::RuleType::IpCidr(net)]
} else if let Ok(net) = format!("{ip_str}/128").parse::<ipnet::IpNet>() {- 位置:
crates/tuic-server/src/acl.rs:627-636 - 类别: security / 置信度: high
- 描述:无效 IP 进入
0.0.0.0/32,可能导致路由错配或 fail-open。 - 建议:返回
Result并在 ACL 加载失败时拒绝启动。 - 证据:
vec![wrule::RuleType::IpCidr(
ip_str.parse().unwrap_or_else(|_| "0.0.0.0/32".parse().unwrap()),
)]- 位置:
crates/tuic-server/src/acl.pest:41-44 - 类别: correctness / 置信度: high
- 描述:
(ASCII_HEX_DIGIT | ":")+匹配10/cafe/::::,排在 ipv4 前导致proxy 10被识别为 IPv6 并保存为字符串,运行时不匹配。 - 建议:真实 IPv6 语法或解析时用
Ipv6Addr::from_str校验,并把更具体的规则提前。 - 证据:
ipv6 = @{ (ASCII_HEX_DIGIT | ":")+ ~ &(WHITESPACE | EOI) }- 位置:
crates/tuic-server/src/acl.pest:9, 15-25 - 类别: correctness / 置信度: medium
- 描述:
wildcard_domain | cidr | ipv6 | ipv4 | domain | any_addr顺序与过宽的 domain/ipv6 共同导致1.2.3等被当成 domain 存储,无声不匹配。 - 建议:收紧 ipv6,提前 cidr,解析后做合法性校验。
- 证据:
address = { localhost_kw | private_kw | suffix_localhost | wildcard_domain | cidr | ipv6 | ipv4 | domain | any_addr }- 位置:
crates/tuic-client/src/wind_adapter.rs:29-34 - 类别: security / 置信度: high
- 描述:直接
lookup_host(...).next(),忽略 V4first/V6first/V4only/V6only;V4only/V6only 可能选到禁用族。 - 建议:使用
utils::ServerAddr::resolve()或复制其过滤逻辑。 - 证据:
let addrs = tokio::net::lookup_host(format!("{}:{}", relay.server.0, relay.server.1)).await?;
addrs.into_iter().next().ok_or_else(|| eyre::eyre!("Failed to resolve server address"))?- 位置:
crates/tuic-client/src/socks5/mod.rs:37-52 - 类别: concurrency / 置信度: medium
- 描述:返回
Result的函数却.unwrap(),二次设置 OnceCell 直接 panic。 - 建议:映射为
Error变体返回。 - 证据:
SERVER.set(Self::new(...)?).map_err(|_| "failed initializing socks5 server").unwrap();- 位置:
crates/tuic-client/src/socks5/handle_task.rs:143 - 类别: concurrency / 置信度: medium
- 描述:assoc_id 是 u16 易回绕碰撞,远端关闭路径下 remove 返回 None 引发 panic 终止 SOCKS5 连接处理。
- 建议:
let _ = ...remove(&assoc_id);并扩宽 id。 - 证据:
UDP_SESSIONS.get().unwrap().write().await.remove(&assoc_id).unwrap();decode_address用buf.chunk()做索引读取,非 contiguous 安全 —crates/wind-tuic/src/proto/mod.rs:85-135,在多 chunk 时可 panic。- 协议重复实现的 decode helpers 与权威 codec 分歧 —
crates/wind-tuic/src/proto/mod.rs:29-42, 45-82, 85-135,易导致解析分歧。 send_packet非分片路径下小max_datagram_size触发下溢 —crates/wind-tuic/src/proto/udp_stream.rs:229,改用saturating_sub。- 未知 UUID 报错携带尝试值并产生时序 oracle —
crates/wind-tuic/src/quinn/inbound.rs:664-666,统一为 "Invalid authentication"。 - acceptor_loop 把正常断连记为 error —
crates/wind-tuic/src/quinn/inbound.rs:75-82。 - TLS 私钥 loader 把任意非空字节当 PKCS8 —
crates/tuic-server/src/tls.rs:109-121。 - TuicheInbound builder 收集
max_idle_time/users但不转发 —crates/wind-tuic/src/quiche/inbound.rs:113-128。 TlsConfig类型定义后未引用 —crates/wind-tuic/src/quiche/tls.rs:1-24。- 65KiB 栈缓冲、未用的
_out—crates/wind-tuic/src/quiche/inbound.rs:66-67。 - DST/SRC 端口范围允许 lo>hi 静默不匹配 —
crates/wind-core/src/rule.rs:367。 SUB-RULE丢弃额外子规则并丢失 name —crates/wind-core/src/rule.rs:625-631。NOT/SUB-RULE的 Display 输出多余括号无法回环解析 —crates/wind-core/src/rule.rs:795-796。compat::TokioTcpCompat模块未使用 —crates/wind-core/src/outbound.rs:19-57。- SOCKS5 UDP
FRAG字节被丢弃 —crates/wind-socks/src/udp.rs:37-40,违 RFC §7。 parse_udp_request_sync失败时仍写入source_addr—crates/wind-socks/src/udp.rs:104-133。- Cronet 库搜索路径仅适配 Linux —
crates/wind-naive/src/lib.rs:364-369。 init_log对wind_naive/wind_dns/wind_acme等 crate 未生效 —crates/wind/src/log.rs:5-26。WIND_OVERRIVE_VERSION环境变量名拼错 —crates/wind/src/main.rs:96-99。- ACME 后台任务为 fire-and-forget,缺少取消与失败可见性 —
crates/wind-acme/src/lib.rs:81-113。 - 推断格式时文件被读了两次,存在 TOCTOU —
crates/tuic-server/src/config.rs:763-779。 format_optional_parts/format_protocol在 Display 时分配中间 String —crates/tuic-server/src/acl.rs:30-39, 89-94。Error变体大量未构造,疑似遗留 —crates/tuic-server/src/error.rs:20-31。AppContext.cancel仅被装饰使用,从未触发 —crates/tuic-server/src/lib.rs:20-39。forward.rs中UDP_SESSIONS注册表只写不读,纯死代码 —crates/tuic-client/src/forward.rs:32-38。- 默认 SNI 取
server.0即便是 IP 字面量 —crates/tuic-client/src/wind_adapter.rs:42。 Error::Timeout/WrongPacketSource/Socks5变体未构造 —crates/tuic-client/src/error.rs:17-26。
proto/cmd.rs:95-100, 109:assoc_id: assos_id是拼错的重命名,消除。proto/mod.rs:149-164:encode_and_send_uni同时接cmd_type与command,易传不一致;直接从(&command).into()推导。proto/udp_stream.rs:44, 46:next_pkt_id/fragment_buffer注释提到不存在的 Mutex。quiche/utils.rs:86-108:QuicheError变体全部未构造。quinn/outbound.rs:53-55:rustls::crypto::*::install_default()在每次构造时重复调用,改为OnceLock。quinn/inbound.rs:75-82:error!对ApplicationClosed/LocallyClosed/TimedOut等普通断连噪音过大。socks/ext.rs:88:try_join!Ok 分支warn!("unreachable")误导。socks/inbound.rs:77:SocksInbound::new标async但无 await。tls.rs:162-163:测试辅助函数中的key_pair.serialize_der()是死调用。naive/lib.rs:322-333:每包重新分配 64KiB 缓冲;lib.rs:232-233:target.to_string()/payload.to_vec()每包重复分配。main.rs:87-93:clap 解析错误打到 stdout 且退出码 0,应用err.exit()。tuic-server/config.rs:1645:assert!(opt.is_none() || opt.is_some())永真。tuic-client/forward.rs:89-93:正常监听启动信息使用warn!。
关联问题:默认 skip_cert_verify = true、0-RTT 无重放保护、SOCKS5 UDP 中继接受任意源、tuic-client UDP 关联首发包竞争、ALPN 硬编码 h3、未知 UUID 错误差异化与时序 oracle、ACME 任务无可观察失败、TLS 私钥 loader 接受任意字节。这一组直接影响产品的安全语义,且修复风险可控。
关联问题:Bytes::copy_to_bytes 缺校验、QUIC uni 流未 finish()、buf.chunk() 索引非 contiguous 安全、重复的 decode_* helper、send_datagram Chain 拷贝、分片 saturating_sub 除零、saturating_sub 漏用、send_packet 非原子 pkt_id、攻击者可控分片字段。重点是协议层的健壮性。
关联问题:quinn/inbound UDP 会话驱逐不取消任务、容量 u16::MAX、spawn_handler 1s 超时杀连接、tokio::select! endpoint.accept() None panic、tuic-server ctrl-c 未 cancel、TLS watcher 错误退出循环、hash 在 reload 前 commit、udp_assoc_counter 回绕、socks/udp 关联 source_addr 在 parse 前写入、forward.rs 每包 spawn、Naive UDP 每包新隧道、Naive 无界 channel、直连 UDP 任务 leak、UDP_SESSIONS.remove().unwrap() panic。优先解决会话生命周期闭环。
关联问题:rule_target_to_action 小写化错配、UDP 调度统一哨兵导致 ACL 失效、空 AND/OR 悖论、IP-SUFFIX 与 IP-CIDR 重复、DomainSuffix/Keyword 每次分配、wildcard_match 每次编译正则、NOT/SUB-RULE 显示不可回环、SUB-RULE 丢内容、端口范围 lo>hi、TargetAddr 反序列化、copy_io half-close、tuic-server 配置 send/receive_window rename 互换、格式推断 = 误判、扩展名分流、attempts 缺省、acl.pest 文法过宽、AclAddress::Ip /32 误用。覆盖最大用户面错配。
关联问题:info! 在每包打印、format_optional_parts 分配、rustls::install_default() 重复调用、StackPrefer 重复定义、compat::TokioTcpCompat 死模块、TlsConfig 未引用、QuicheError/Error 未构造、UDP_SESSIONS 注册表只写不读、init_log 漏 crate、WIND_OVERRIVE_VERSION 拼写、clap 错误流、tuic-client/wind_adapter 默认 SNI、各种注释/命名 nit。集中清理可显著降低维护成本。