v0.3.1
XferRust v0.3.1 Release Notes
English | 简体中文
Summary
- Fixed BT public-swarm downloads being far slower than other clients (same public torrent: <1 MB/s → 17.1 MB/s peak): three root causes — IPv6 peers dropped by trackers, DHT dual-stack binding always failing, and DHT queries always timing out
- IPv6 coverage completed: bare IPv6 literals in non-compact tracker peer lists and the BEP 32
peers6field, IPv4 + IPv6 announces per tracker merged and deduplicated, and dual-stack uTP / UDP tracker / BT listening (none of which could even start on IPv6 hosts) - HTTPS now also trusts the local system root store (corporate proxies and private CAs no longer fail across the board) and
no-proxyreally takes effect - Fixed HTTP split downloads reporting success without downloading a single byte when the control file's segment table is unusable (leaving a truncated file behind while showing as complete); the resume bitfield no longer runs ahead of disk, so a crash recovery with
disk-cacheno longer reproduces "completed holes" - Fixed three DHT protocol errors: responses not validated against the source address (guessing a 16-bit transaction id was enough to inject a forged one),
announce_peeralways sent withimplied_port=1(making the remote node record our DHT port as our BT port), andget_peersencoding IPv6 peers as0.0.0.0 - Fixed the on-disk layout of multi-file torrents with exactly one file (the file was written as
<name>, occupying what should be the directory name; data downloaded by older versions migrates automatically) and thefiles[].path/task.verifyFilespath for single-file torrents (<name>/<name>→<name>) - Fixed NAT-PMP TCP port mapping never working (opcode 6 → 2 per RFC 6886), DHT routing-table persistence always panicking, and peer-id generation panicking whenever a version component has two digits
- Faster cold start and lower idle cost: DHT query timeout 10s → 3s with bootstrap and one iteration of find_node running concurrently, and release builds optimized for speed (see "Build & Release")
New Features
- Per-task custom request headers are now actually sent: the
headeroption (an array like["Referer: https://…", "Cookie: a=b"], or a single CRLF/LF-separated string) and thereferer/user-agentconvenience keys are applied to real HTTP requests — previously they were collected into the task's options and never used when downloading (callers believed they were sent, but they were silently dropped), so every URL that requiresReferer/Cookiefailed with 403. The probe, single-connection and multi-connection split paths all use the same set (sending them on the download but not on the probe makes the probe 403 and misjudge the total length and Range support);Range/Host/Content-Length/Connection/Accept-Encodingare always ignored (overriding them breaks segmenting, resume and virtual-host routing), andOriginis never injected
Bug Fixes
- Fixed IPv6 addresses in non-compact peer lists being dropped: the address and port were concatenated into
ip:portand re-parsed, which always fails for bare IPv6 literals (the port gets swallowed into the address) and discarded the whole peer; peers are now parsed from the IP literal with the port assembled afterwards, so a single invalid entry only skips that entry - Fixed the tracker
peers6field being ignored: added BEP 32 compact parsing (18 bytes per entry = 16-byte IPv6 + 2-byte port, port 0 dropped), with a ragged tail truncated and warned about - Fixed never obtaining IPv6 seeders: hostnames resolve to IPv4 first, so announces were always sent over IPv4 while trackers only return IPv6 peers to IPv6 sources (BEP 7); every tracker is now announced to over both IPv4 and IPv6 concurrently with the results merged and deduplicated — skipped automatically when the host has no IPv6 address or is a bare IP, and the IPv6 result is used as a fallback when the IPv4 announce fails
- Fixed DHT dual-stack nodes always failing to start on macOS: the
[::]bind address was handed to the resolver as a hostname (and then fell back to IPv4-only); it is now parsed as an IP literal (brackets stripped) - Fixed address-family mismatch when sending from / receiving on a dual-stack DHT socket: IPv4 destinations are converted to v4-mapped before sending and v4-mapped sources are normalized back to IPv4, keeping the routing table, known_peers and peer addresses handed to BT as IPv4
- Fixed every DHT KRPC query timing out: callers raced the resident receive loop for the same socket, and responses picked up by the loop were parsed as queries, failed, and silently discarded, leaving callers to time out; pending queries are now registered as "transaction id → channel" and the receive loop dispatches responses by tid (the entry is removed on send failure or timeout)
- Fixed slow DHT cold start: find_node queries for all bootstrap nodes and one iteration now run concurrently
- Fixed uTP being unable to talk to IPv6 peers: the uTP socket was bound to IPv4 only, and on a dual-stack listener every send to an IPv6 / IPv4 peer needs an address-family conversion — without it sendto fails with
Invalid argument (os error 22)and the uTP dial to an IPv6 peer can never succeed (each peer wastes a full handshake timeout, measured at 3s per peer, before falling back to TCP); the socket is now bound dual-stack to[::]with v4-mapped conversion on both send and receive - Fixed BT being unable to listen on IPv6-only hosts (or without IPv6): the TCP listener was bound to
0.0.0.0(tasks failed to start at all on IPv6-only hosts, and dual-stack hosts never received IPv6 inbound connections); it now prefers the dual-stack[::]and adds a plain IPv4 listener on systems withIPV6_V6ONLY=1(e.g. the Windows default); the conflict from that extra bind is ignored when the dual-stack socket already covers IPv4 - Fixed UDP trackers being IPv4-only: the socket was bound to
0.0.0.0, so on IPv6-only hosts everyudp://tracker was unusable and IPv6 trackers failed to send; it is now dual-stack with the same send-side conversion - Fixed v4-mapped peer addresses not being normalized: some trackers / PEX implementations send IPv4 addresses as
::ffff:a.b.c.d, which used to create a second peer record alongside the IPv4 literal and could never be dialed on hosts without an IPv6 route; addresses are now normalized to IPv4 before being stored - Fixed HTTPS failing across the board with private CAs / corporate proxies (TLS interception): TLS only trusted the Mozilla roots compiled into the binary and the local OS certificate store (corporate CAs, user-imported certificates) took no part in verification; system roots are now loaded as well (a union with the built-in roots, leaving public-site verification results unchanged)
- Fixed the
no-proxyoption having no effect: it was stored but never used to build the HTTP client, so in proxied environments LAN / loopback addresses listed inno-proxywere still pushed through the proxy and hosts that were directly reachable failed instead; it now really acts as the proxy's direct-connection exception (when using environment-variable proxies, theNO_PROXYenvironment variable applies) - Fixed HTTP split downloads being reported as finished when the control file's segment table is unusable: when the table was not a strict tiling (gap / overlap / empty) the writer neither fell back nor rebuilt it, so
todocame out as 0 and the download was markedfinished— success without a single byte downloaded, and the target file was not even resized to the correct length; an invalid control file is now treated exactly like "no control file" (the existing file is taken as a contiguous prefix and the remainder is split again) - Fixed the resume bitfield running ahead of disk: with
disk-cacheenabled, piece data lands in an in-memory write-back buffer first while the resume control file is written as soon as a piece completes — so the bitfield could declare pieces complete that had not written a single byte, and after a crash / power loss recovery skipped them, producing "completed holes" (silent corruption). The persisted bitfield now excludes pieces still in the write-back buffer (the only cost is re-downloading those few pieces after a crash), and a clean pause flushes to disk before writing the bitfield - Fixed DHT KRPC responses not being validated against the source address: the pending-query table was keyed by the 16-bit transaction id alone, so with hundreds of concurrent queries in one round (bootstrap + iterative find_node) the birthday bound gives a noticeable chance of a collision delivering a response to the wrong node's waiter, and any host guessing a tid could inject a forged response (peers go straight into the BT dial queue); the key is now "source address + tid" and responses with no waiter no longer enter the query-handling path
- Fixed NAT-PMP TCP port mapping never working: the opcode was written as 6 (neither 1 nor 2), so gateways always answered unsupported and TCP mapping depended entirely on the UPnP fallback; it is now 2 per RFC 6886 §3.3
- Fixed DHT
announce_peeralways being sent withimplied_port=1: BEP 5 says that when the bit is 1 the remote node ignores theportargument and uses the UDP source port, and announces leave through the DHT socket (a different port from the BT listen port) — so the remote node recorded our DHT port as our BT port, spreading a dead address; announces now declareimplied_port=0with the real BT listen port, and the self-address recorded in known_peers uses that port too - Fixed DHT
get_peersreplies encoding IPv6 peers as0.0.0.0: every peer was encoded as a 6-byte IPv4 compact entry, so the 4 IP bytes of an IPv6 address were filled with zeros (a non-zero port still passed the receiver-side filter) and clients receiving it would try to connect to0.0.0.0:port; entries are now split per BEP 5 / BEP 32 — IPv4 intovalues, IPv6 intopeers6(18 bytes per entry) - Fixed DHT routing-table persistence panicking as soon as it is enabled: both the periodic saver and
shutdowncalledRwLock::blocking_read, which panics inside an asynchronous execution context ("Cannot block the current thread from within a runtime"); the periodic saver now uses the async read lock and the synchronous entry point usestry_read, skipping the round when the lock is held - Fixed multi-file torrents with exactly one
filesentry being written as a plain file at<name>: single-file vs multi-file was decided byfiles.len() == 1 && path.len() == 1, which cannot tell a real single-file torrent (info haslength) from a one-entryfileslist (info hasfiles) — so the file landed at<name>, occupying what should be the directory name, with its own path segment dropped, and the on-disk layout no longer matched other clients (breaking seeding); the decision now comes from the torrent structure itself (anInfo::multi_fileflag threaded through parsing → layout → storage → verification) - Data downloaded by older versions is migrated automatically: the first time such a task is opened, the plain file at
<name>is moved into<name>/<file>, keeping resume and seed data; if the location is occupied by an unrelated file the engine fails loudly instead of moving it - Fixed
files[].pathand thetask.verifyFilesverification path for single-file torrents: both were uniformly built as{name}/{path}, which yields<name>/<name>for a single-file torrent — a path that does not exist, so the detail page showed a wrong location and verification always reported the file as missing; both now go throughInfo::file_rel_path(single-file =<name>, multi-file =<name>/<path>), matching the on-disk layout - Fixed peer-id generation panicking for two-digit version components: the prefix was assembled with
format!("-XR{maj}{min}{mic}0-"), so 0.10.0 produced a 9-byte prefix andcopy_from_slicefailed on a length mismatch (debug_assertdoes not exist in release, so upgrading to 0.10.x meant panicking the first time every task generated a peer-id); the prefix is now built into a fixed 8-byte array (digits for 0-9, letters for 10-35, saturating beyond), making the overflow structurally impossible
Behavior Changes
- DHT KRPC query timeout tightened from 10s to 3s: unreachable nodes fail faster and bootstrapping / get_peers iterations are no longer stalled by slow nodes
- Each HTTP tracker now receives two announce requests per round (IPv4 + IPv6): tracker-side request volume doubles in exchange for all seeders that are only returned to IPv6 sources; no extra request is made when IPv6 is unavailable
no-proxygoes from "recorded only" to actually taking effect: hosts matching that list no longer go through the proxy configured byall-proxy- The HTTPS trust chain is relaxed to the extent of the local system root store (union semantics): certificates trusted by the local machine are accepted by the engine as well, while public-site verification results are unchanged
- DHT responses are only accepted when they come from the address that was queried: responses from any other address (forged / stale) are dropped before reaching the query handling path
- An unusable HTTP control-file segment table is no longer restored as-is but takes the same path as "no control file": the existing file is treated as a contiguous prefix and the remainder is split into fresh segments (a few overlapping bytes may be re-downloaded, in exchange for never producing a truncated file that is reported as complete)
- With
disk-cacheenabled the resume bitfield no longer claims pieces that have not been flushed: after a crash recovery those pieces are downloaded again (previously this silently produced a corrupt file); clean pause / stop still flushes before writing the bitfield, so progress is unaffected - Single-file vs multi-file is now decided by the torrent structure (whether info carries
lengthor afileslist): a multi-file torrent with a single file moves from "a file occupying the directory name" to living inside the<name>/directory, matching qBittorrent / aria2; data downloaded by older versions migrates automatically on upgrade files[].pathfor single-file torrents is now<name>instead of<name>/<name>(matching aria2); clients that name tasks frombittorrent.info.nameare unaffected
Build & Release
- Engine version bumped to 0.3.1 (
engine.getVersionreports 0.3.1) - New dependency
rustls-native-certs(reads the OS certificate store per platform: macOS Security.framework, Windows schannel, Linux /etc/ssl with openssl-probe) - release builds now optimize for speed (
opt-level=3) instead of size (opt-level="s"): BT send/receive, SHA-1 verification and piece I/O hot paths get measurably faster; size stays controlled bylto+strip, keeping the single-file release - Idle I/O and wakeup reductions: the 30s periodic session save no longer rewrites the whole session file when its content is unchanged (piece bitfields can reach hundreds of KB, so idle saves were pure repeated writes); the uTP manager's tick widens from 1ms to 100ms with no connections and returns immediately on connect / inbound, leaving retransmission and SACK timers untouched
XferRust v0.3.1 发布说明
English | 简体中文
摘要
- 修复 BT 公网下载速度远低于其他客户端(同一公开种子实测 <1MB/s → 17.1MB/s):tracker 丢弃 IPv6 peer、DHT 双栈绑定必然失败、KRPC 查询必然超时三处根因
- IPv6 补全:tracker 非 compact 列表的裸 IPv6 字面量与 BEP 32
peers6解析、IPv4 + IPv6 双路 announce 合并去重,uTP / UDP tracker / BT 监听全部双栈化(IPv6 主机上此前起不来) - HTTPS 补齐本机系统根证书(企业代理 / 自签 CA 环境不再全线失败);
no-proxy真正生效 - 修复 HTTP 分片下载在控制文件段表不可用时一个字节都没下就报完成(磁盘上留下残缺文件却显示成功);续传位图不再领先于磁盘,开启
disk-cache后崩溃恢复不再产出「已完成的空洞」 - 修复 DHT 三处对外行为错误:响应不校验来源地址(猜中 16 位事务 id 即可注入伪造响应)、
announce_peer恒宣告implied_port=1(对端把 DHT 端口记成我们的 BT 端口)、回get_peers把 IPv6 peer 编成0.0.0.0 - 修复「只有 1 个文件的多文件种子」落盘布局(文件被写成
<name>占了目录名,旧版已下载数据自动迁移)与单文件种子的files[].path/task.verifyFiles路径(<name>/<name>→<name>) - 修复 NAT-PMP 的 TCP 端口映射从未生效(opcode 6 → RFC 6886 规定的 2)、DHT 路由表保存必 panic、版本号出现两位数时 peer-id 生成必 panic
- 冷启动更快、空闲更省:DHT 查询超时 10s → 3s 且 bootstrap 与一轮迭代的 find_node 改为并发,release 构建改按性能优化(详见「构建与发布」)
新特性
- 逐任务自定义请求头真正下发:
header选项(["Referer: https://…", "Cookie: a=b"]数组,或按 CRLF / LF 分行的字符串)与referer/user-agent便捷键现在会带到实际 HTTP 请求上——此前这些选项只被收进任务选项、下载时从未使用(客户端侧以为发了,实际静默丢弃),于是所有「必须有Referer/Cookie才能访问」的地址一律 403。探测、单连接、多连接分片三条路径使用同一组头(只给下载带头、探测不带,会因探测 403 判错总长与 Range 支持);Range/Host/Content-Length/Connection/Accept-Encoding一律忽略(覆盖它们会破坏分段、续传与虚拟主机路由),且不会注入Origin
问题修复
- 修复非 compact peer 列表中的 IPv6 地址被丢弃:此前把地址与端口拼成
ip:port再解析,裸 IPv6 字面量必然失败(端口被吞进地址)导致整条 peer 丢失;现先按 IP 字面量解析再组装端口,单条非法只跳过该条 - 修复 tracker 的
peers6字段被忽略:新增 BEP 32 compact 解析(18 字节/条 = 16 字节 IPv6 + 2 字节端口,端口为 0 丢弃),长度非 18 倍数时截断尾部并告警 - 修复始终拿不到 IPv6 seeder:域名解析 IPv4 优先,announce 必然从 IPv4 发出,而 tracker 只把 IPv6 peer 回给 IPv6 来源(BEP 7);现每个 tracker 并发发起 IPv4 / IPv6 两路 announce 并合并去重,域名无 IPv6 地址或为 IP 直连时自动跳过,IPv4 失败时回退 IPv6 结果
- 修复 DHT 双栈节点在 macOS 上必然启动失败:绑定地址
[::]此前作为域名交给解析器(报错后回退纯 IPv4);现按 IP 字面量解析(自动剥离方括号) - 修复双栈 DHT socket 收发地址族不匹配:发往 IPv4 节点前转 v4-mapped、接收到的 v4-mapped 源地址还原为 IPv4,路由表、known_peers 与交给 BT 的 peer 地址仍保持 IPv4
- 修复 DHT 所有 KRPC 查询必然超时:查询方与常驻接收循环争抢同一 socket,响应被接收循环取走后按查询解析失败静默丢弃,调用方只能等到超时;现以「事务 id → 通道」登记待响应查询,接收循环识别响应后按 tid 投递(发送失败或超时即摘除登记项)
- 修复 DHT 冷启动慢:bootstrap 各节点与一轮迭代的 find_node 改为并发执行
- 修复 uTP 无法与 IPv6 对端通信:uTP socket 此前绑纯 IPv4,双栈监听后向 IPv6 / IPv4 对端发送都需地址族转换——不转换时 sendto 报
Invalid argument (os error 22),与 IPv6 对端的 uTP 拨号必然失败(每个对端白等一轮握手超时才回退 TCP,日志实测 3s/对端);现绑定[::]双栈 socket 并在收发两端做 v4-mapped 转换 - 修复无 IPv6 或 IPv6-only 主机上 BT 无法监听:TCP 监听此前绑
0.0.0.0(IPv6-only 主机上任务启动即失败,双栈主机收不到 IPv6 入站连接);现优先绑[::]双栈,并在IPV6_V6ONLY=1的系统(如 Windows 默认)上补一个纯 IPv4 监听,双栈已覆盖时该补充绑定冲突会被忽略 - 修复 UDP tracker 只支持 IPv4:socket 此前绑
0.0.0.0,IPv6-only 主机上udp://tracker 全部不可用、IPv6 tracker 发送失败;现双栈绑定并做发送地址转换 - 修复 v4-mapped 形式的对端地址未归一:部分 tracker / PEX 用
::ffff:a.b.c.d传 IPv4 地址,此前会与 IPv4 字面量形成两条对端记录,且在没有 IPv6 路由的环境里拨号必然失败;现入库前统一还原为 IPv4 - 修复 HTTPS 在自签 CA / 企业代理(TLS 拦截)环境全部失败:TLS 此前只信任编译期内置的 Mozilla 根证书,本机系统证书仓库(含企业 CA、用户导入证书)完全不参与校验;现同时加载系统根证书(与原内置根证书取并集,不改变公共站点校验结果)
- 修复
no-proxy选项不生效:该选项此前只被存储、从未用于构建 HTTP 客户端,代理环境里no-proxy列出的局域网 / 回环地址仍被塞进代理,本可直连的地址反而失败;现作为代理的直连例外真正生效(走环境变量代理时由NO_PROXY环境变量控制) - 修复 HTTP 分片下载的控制文件段表不可用时被判为已完成:段表非严格平铺(有间隙 / 重叠 / 空表)时既不回落也不重建段表,
todo因此算出 0 → 直接finished——一个字节都没下载就报成功,且目标文件不被截齐到正确长度;现校验不过的控制文件一律等价于「无控制文件」(把已有文件当连续前缀、重新按剩余量切段) - 修复续传位图领先于磁盘:开启
disk-cache后片数据先落入内存回写缓冲,而片完成时立即写续传控制文件——此时位图声明的已完成片可能一个字节都没落盘,崩溃 / 断电恢复后按位图跳过这些片,产出「已完成的空洞」(静默损坏)。现持久化位图排除仍在回写缓存中的片(代价仅是崩溃后重下缓存内那几个片),干净暂停改为先flush_all落盘再写位图 - 修复 DHT KRPC 响应不校验来源地址:待响应表仅以 16 位事务 id 为键,一轮并发查询(bootstrap + 迭代 find_node 可达数百个)按生日界就有可观概率撞号把响应错投给另一个节点的等待者,任意主机只要猜中 tid 也能注入伪造响应(peer 会直接进入 BT 连接队列);现按键改为「来源地址 + tid」,且无等待者的响应不再进入查询处理路径
- 修复 NAT-PMP 的 TCP 端口映射从未生效:opcode 写成 6(既非 1 也非 2),网关一律回不支持,TCP 映射全靠 UPnP 回退兜底;按 RFC 6886 §3.3 改为 2
- 修复 DHT
announce_peer恒以implied_port=1发送:BEP 5 规定该位为 1 时对端忽略port参数、改用 UDP 源端口,而本端 announce 走的是 DHT socket(端口与 BT 监听端口不同)——对端因此把 DHT 端口记成我们的 BT 端口,散播出去的是没人监听的死地址;现显式声明implied_port=0并带上真实 BT 监听端口,本端记入 known_peers 的自身地址也改用该端口 - 修复 DHT 回
get_peers时把 IPv6 peer 编成0.0.0.0:所有 peer 统一按 6 字节 IPv4 compact 编码,IPv6 地址的 4 个 IP 字节被填 0(端口非 0 能过接收侧过滤),拿到它的客户端会把0.0.0.0:port当 peer 去连;现按 BEP 5/BEP 32 拆分——IPv4 进values、IPv6 进peers6(18 字节/条) - 修复 DHT 路由表保存会在启用持久化后必然 panic:定期保存任务与
shutdown都调用RwLock::blocking_read,而它跑在异步执行上下文里("Cannot block the current thread from within a runtime");现定期保存改走异步读锁,同步入口改用try_read并在锁被占用时跳过本轮 - 修复「只有 1 个
files条目的多文件种子」落盘成<name>普通文件:此前以files.len() == 1 && path.len() == 1判定单文件,这种种子(info 里是files列表而非length)的文件被写到<name>,占了目录名且文件自身的路径段被整个丢掉,与其他客户端的磁盘布局不一致(无法继续做种);现以 info 字典写的是length还是files为准(Info::multi_file结构位贯穿解析 → 布局 → 落盘 → 校验),文件按<name>/<path>落盘 - 旧版已下载的此类数据自动迁移:升级后首次打开该任务时把
<name>(普通文件)搬进<name>/<file>,续传与做种数据不丢;位置被外来文件占用时明确报错而非误搬 - 修复单文件种子的
files[].path与task.verifyFiles校验路径:两者此前统一按{name}/{path}拼接,单文件种子得到<name>/<name>(不存在的路径)——详情页显示错误位置、校验必然报 missing;现统一走Info::file_rel_path(单文件 =<name>,多文件 =<name>/<path>),与落盘布局同口径 - 修复版本号出现两位数时生成 peer-id 必然 panic:前缀此前用
format!("-XR{maj}{min}{mic}0-")拼接,0.10.0 会拼出 9 字节前缀,copy_from_slice长度不匹配越界(debug_assert在 release 不生效,等于升到 0.10.x 后每个任务第一次生成 peer-id 就崩);现按 4 个字符位定长构造(0-9 → 数字、10-35 → 字母、超出饱和),结构上不可能越界
行为变更
- DHT KRPC 查询超时由 10s 收紧到 3s:不可达节点更快判定失败,bootstrap 与 get_peers 迭代不再被慢节点拖住
- 同一 HTTP tracker 每轮会收到两次 announce 请求(IPv4 + IPv6):tracker 侧请求量翻倍,换取只回给 IPv6 来源的全部 seeder;无 IPv6 可用时不产生额外请求
no-proxy由「仅记录」变为真正生效:命中该列表的主机不再走all-proxy配置的代理- HTTPS 信任链在本机系统根证书范围内放宽(并集语义):本机被信任的自签证书也会被引擎接受,公共站点的校验结果不变
- DHT 响应必须来自被查询的那个地址才会被采纳:来自其他地址的响应(伪造 / 过期)直接丢弃,不再进入查询处理流程
- 段表不可用的 HTTP 控制文件不再按原表恢复,改为与「无控制文件」同路径:已有文件视作连续前缀、剩余部分重新切段(可能重下少量重叠字节,换取不产出假完成的残缺文件)
- 开启
disk-cache时续传位图不再声明尚未落盘的片:崩溃恢复后这些片会被重新下载(此前是静默产出损坏文件);正常暂停 / 停止路径仍先落盘再写位图,进度不受影响 - 单文件与多文件种子的判据改为种子结构位(info 写
length还是files):只有 1 个文件的多文件种子从「文件占目录名」改为「落在<name>/目录里」,与 qBittorrent / aria2 等客户端布局一致;旧版已下载的数据在升级后自动迁移,无需重下 - 单文件种子的
files[].path由<name>/<name>修正为<name>(与 aria2 一致):客户端按bittorrent.info.name取名的行为不变
构建与发布
- 引擎版本升至 0.3.1(
engine.getVersion返回 0.3.1) - 新增依赖
rustls-native-certs(读取各平台系统证书仓库:macOS Security.framework、Windows schannel、Linux /etc/ssl + openssl-probe) - release 构建从按体积优化(
opt-level="s")改为按性能优化(opt-level=3):BT 收发、SHA-1 校验、分片读写等热点更快;体积仍由lto+strip控制,保持单文件发布 - 空闲时 IO 与唤醒优化:会话内容无变化时 30s 定期保存不再重写整份会话文件(分片位图可达数百 KB,空闲时纯属重复写入);uTP 管理器无连接时 tick 由 1ms 放宽到 100ms,建连 / 入站路径即时恢复,重传与 SACK 定时不受影响