跳转到内容

协议 crate 基础

源码文件:47 个 · 核对版本 Etemenanki 596916d · katana v3.0.1
  • Etemenanki/protocols/Cargo.toml
  • Etemenanki/protocols/src/lib.rs
  • Etemenanki/protocols/src/flow.rs
  • Etemenanki/protocols/src/error.rs
  • Etemenanki/protocols/src/macros.rs
  • Etemenanki/protocols/src/core/mod.rs
  • Etemenanki/protocols/src/core/harness.rs
  • Etemenanki/protocols/src/http/core.rs
  • Etemenanki/protocols/src/ss_legacy/core.rs
  • Etemenanki/protocols/src/ss_2022/core.rs
  • Etemenanki/protocols/src/vmess/core.rs
  • Etemenanki/protocols/src/tun/udp.rs
  • Etemenanki/protocols/src/helpers/address.rs
  • Etemenanki/protocols/src/helpers/parse.rs
  • Etemenanki/protocols/src/helpers/crypto.rs
  • Etemenanki/protocols/src/helpers/address_family.rs
  • Etemenanki/protocols/src/sniff/mod.rs
  • Etemenanki/protocols/src/sniff/collector.rs
  • Etemenanki/protocols/src/mux/demux.rs
  • Etemenanki/protocols/src/mux/frame.rs
  • Etemenanki/protocols/src/trojan/core.rs
  • Etemenanki/protocols/src/trojan/protocol.rs
  • Etemenanki/protocols/src/trojan/users.rs
  • Etemenanki/protocols/src/vless/protocol.rs
  • Etemenanki/protocols/src/vmess/protocol.rs
  • Etemenanki/protocols/src/vmess/aead.rs
  • Etemenanki/protocols/src/socks/protocol.rs
  • Etemenanki/protocols/src/socks/server.rs
  • Etemenanki/protocols/src/ss_legacy/protocol.rs
  • Etemenanki/protocols/src/ss_2022/protocol.rs
  • Etemenanki/protocols/src/tun/inbound.rs
  • Etemenanki/protocols/tests/pipeline.rs
  • Etemenanki/protocols/tests/pipeline/core.rs
  • Etemenanki/protocols/tests/unit/core/mod.rs
  • Etemenanki/protocols/tests/unit/helpers/address.rs
  • Etemenanki/protocols/tests/unit/helpers/address_family.rs
  • Etemenanki/app/Cargo.toml
  • Etemenanki/app/src/flow.rs
  • Etemenanki/app/src/router.rs
  • Etemenanki/app/src/serve.rs
  • Etemenanki/app/src/config.rs
  • Etemenanki/app/src/inbound/mod.rs
  • Etemenanki/app/src/outbound/mod.rs
  • Etemenanki/app/src/outbound/udp_fanout.rs
  • Etemenanki/concepts/src/core.rs
  • katana/Cargo.toml
  • katana/src/outbound/mod.rs

etemenanki-protocols 包含全部代理协议和传输层。其中每个服务端协议核心(core)都由同一小组部件组装而成:一个说明连接去向的 Flow<T>,一个用于不可信帧的错误类型,一个含义随阶段变化的定时器,一个存放嗅探字节的缓冲区,一个跟踪半关闭的中继尾段,以及少量解析、地址和加密辅助函数。

本页先逐一介绍这些部件,再说明协议模块的目录布局,以及新增一个协议需要做哪些事。阅读前应了解 ProxyCoreDecode 的 sans-I/O 契约(事件进,effect 出),见服务端协议核心。驱动 core 的 task 见服务端运行时。

这些基础模块负责:

  • 服务端 core 交给 connector(连接器)的流。 它包含目标地址、用户、嗅探到的域名和客户端地址(protocols/src/flow.rs)。
  • 不可信输入的错误分类。 它会桥接到 std::io::Error,因为 concepts 边界固定使用 io::Error(protocols/src/error.rs)。
  • 每个服务端 core 都会组合的部件:FlowKey/SubKey、Phase/Timing、SniffPrefix、Passthrough,以及最小的完整 core PassthroughCore(protocols/src/core/mod.rs)。
  • 不需要 socket 的测试驱动 CoreHarness(protocols/src/core/harness.rs)。
  • 共享的线格式辅助函数:二进制地址编解码器和文本 authority(helpers/address.rs)、不会 panic 的切片操作(helpers/parse.rs)、小型加密原语(helpers/crypto.rs),以及出站的地址族策略(helpers/address_family.rs)。

它们不拥有任何协议的线格式,也不持有 socket 或时钟。core 组合这些部件,而不是继承它们:core 持有一个 Timing 并在每个字节事件的开头调用它;在收集流的首批字节期间持有一个 SniffPrefix;并为其唯一的流式出站持有一个 Passthrough 来记录半关闭状态。

protocols/src/lib.rs 声明了下列模块,其中两个位于默认关闭的 Cargo feature 之后。

模块 开关 内容
core 无 本页介绍的共享 core 部件,以及 CoreHarness。
error、flow、helpers、macros 无 本页介绍的共享类型和辅助函数。
sniff 无 TLS SNI 和 HTTP Host 嗅探器、SNIFF_LIMIT、SNIFF_TIMEOUT。见嗅探。
dns 无 出站拨号时使用的解析器。见 DNS。
transports 无 TCP、TLS、WebSocket 和 gRPC 的 accept 端与 connect 端。见传输层:TCP 与 TLS。
mux 无 Trojan、VLESS 和 VMess 共用的 mux.cool 服务端解复用器。见 mux.cool 与 XUDP。
socks 无 SOCKS4、4a 和 5:使用专门的入站驱动(SocksInbound)而非 core,另有 SOCKS5 客户端 codec 和 UDP link。UDP ASSOCIATE 只接收其控制连接所来自的客户端的数据报;经 Unix socket 时,则只接收请求中给出的确切源地址(ExpectedSender)。见 SOCKS。
http 无 HttpCore 和 HttpConnect 客户端 codec。
trojan、vless、vmess 无 服务端 core 和客户端 codec。
ss_legacy、ss_2022 无 Shadowsocks AEAD 和 Shadowsocks 2022 的 core 与 codec。
wireguard 无 仅出站:基于用户态网络栈的 WgConnector。
hysteria feature = "hysteria" Hysteria 2 客户端和服务端。引入 quinn、h3、rustls 和 blake2。
tun all(feature = "tun", unix) TUN 入站。引入 ipstack、tun-rs,在 Linux 上还有 rtnetlink。

第三个 feature vendored-openssl 转发到 openssl/vendored,不控制任何模块。

这些 feature 默认关闭,是为了避免下游在未主动要求的情况下,因一次版本升级而引入第二套 TLS 栈或网卡管理栈。使用方显式启用:

使用方 启用的 feature
etemenanki-app(app/Cargo.toml) hysteria、tun
katana(Cargo.toml) vendored-openssl、hysteria

protocols/src/lib.rs 开头是一条作用于整个 crate 的 deny:

#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]

这四条 lint 在 cfg(test) 下是允许的,因为测试处理的都是已知合法的输入。workspace 门禁会运行 cargo clippy --workspace --all-targets --all-features -- -D warnings。因此,本 crate 的生产代码不对切片做下标访问,不 unwrap,也不使用未检查的 +、- 或 *。唯一的例外是少数几个函数,它们用局部的 #[allow(..., reason = "...")] 退出 arithmetic_side_effects,并写明运算为何不会溢出:socks/server.rs、transports/ws/stream.rs 和 transports/grpc/liveness.rs 中的 Instant 截止时间辅助函数,以及 gRPC 和 Hysteria 2 的 varint 编解码。不会 panic 的解析中的辅助函数正是因这条规则而存在。

pub struct Flow<T> {
pub destination: Destination,
pub user: NetworkUser<T>,
pub sniffed: Option<SniffedBehavior>,
pub source: Option<IpAddr>,
}
impl<T> Flow<T> {
pub fn new(destination: Destination, user: NetworkUser<T>, source: Option<IpAddr>) -> Self;
pub fn toward(&self, destination: Destination) -> Self;
}

Flow<T> 是本 crate 中每个服务端 core 的 ProxyCoreDecode::Target。core 在 Effect::Open 中推送的就是它,connector 据以路由和拨号的也是它。客户端 codec 则使用 Target = Destination:在客户端一侧,target 是运行时要拨号的上游代理服务器,而流自身的目标地址编码在 codec 内部。

字段 由谁设置 含义
destination core,取自请求 network、remote(IP 或域名)和 port。对 UDP 流而言是第一个包的目标地址,后续的包在 Effect::SendTo 中各自携带地址。
user core,取自其 validator NetworkUser<T>:一个 authorization 加上 user_data: Arc<T>。app 使用 T = ()(app/src/flow.rs → Flow)。katana 等下游携带各自的按用户数据。
sniffed core,嗅探之后 Option<SniffedBehavior>,包含协议和域名。Flow::new 将其置为 None。
source 入站 客户端地址,入站知道时才有。

app 的路由器(app/src/router.rs → route_target)把流转换为路由目标。sniffed 成为目标的嗅探域名,因此按 IP 寻址的流也能匹配域名或 geosite 规则。flow.source 优先于监听器的地址(FlowContext::source),因为 QUIC 入站用一个 socket 服务许多对端,只有流自己知道它属于哪个对端。

toward(destination) 构造一个子流。子流与承载流共享 user(克隆 Arc)和 source,并把 sniffed 重置为 None,因为嗅探到的域名描述的是承载流的首批字节,而不是新的目标地址。调用方有:

  • mux/demux.rs → Demux:每个 mux.cool New 帧对应一个子流。当入站开启嗅探且子流目标是 IP 时,demux 随后会嗅探该 New 帧的负载。
  • trojan/core.rs → TrojanCore:UDP 关联朝第一个包的目标地址打开出站。
  • app/src/outbound/udp_fanout.rs → FanOutLink:把关联中的每个数据报,作为该关联的流、朝该数据报的目标进行路由。

Clone 是手写的而非 derive,因此克隆 Flow<T> 不要求 T: Clone;数据始终位于 Arc 之后。concepts crate 中的 NetworkUser<T> 也是如此,这是为了 katana 的按用户流量计数器,它必须始终是唯一的共享实例。

pub enum ProtocolError {
Truncated(&'static str),
Overflow(&'static str),
Malformed(&'static str),
Unauthenticated(&'static str),
Crypto(&'static str),
Unsupported(&'static str),
Io(#[from] io::Error),
Other(#[from] anyhow::Error),
}
impl From<ProtocolError> for io::Error { /* … */ }
pub type Result<T, E = ProtocolError> = std::result::Result<T, E>;

本 crate 的所有公开服务仍然返回 io::Error,因为这是 concepts 边界。ProtocolError 在解析器内部对失败进行分类,From 实现让任何返回 io::Result 的函数都能用 ? 传播它。&'static str 载荷指明字段或区域,从不携带输入字节。

变体 Display 转换后的 io::ErrorKind
Truncated truncated input: … UnexpectedEof
Overflow integer overflow: … InvalidData
Malformed malformed: … InvalidData
Unsupported unsupported: … InvalidData
Unauthenticated authentication failed: … PermissionDenied
Crypto cryptographic operation failed: … Other
Other 被包装的错误 Other
Io 被包装的错误 被包装的错误,原样返回

在当前版本中,生产代码只构造 Truncated、Overflow 和 Malformed。它们来自切片辅助函数,以及 SOCKS、mux.cool、VMess、两个 Shadowsocks 系列、Hysteria 2 和 DNS 的解析器。Unauthenticated、Crypto 和 Unsupported 已声明但从未抛出。服务端 core 直接以 io::Error 构造认证失败:TrojanCore、VlessCore、VMessCore 和 ShadowsocksCore 使用 PermissionDenied(例如 trojan: invalid user),它们的 core 测试也针对这个 kind 断言。Ss2022Core 把未知身份报告为 InvalidData(shadowsocks-2022: unknown identity),HttpCore 则对登录失败回复 407 响应,而不是返回错误。

pub enum FlowKey {
Direct,
Sub(SubKey),
}
pub struct SubKey {
pub id: u16,
pub generation: u32,
}

两者都 derive 了 Debug、Clone、Copy、PartialEq、Eq、PartialOrd、Ord 和 Hash,满足 ProxyCoreDecode::Key 要求的约束(Copy + Ord + Send + Sync + 'static)。连接上既可能承载单个流、也可能承载 mux.cool 承载连接的 core(TrojanCore、VlessCore、VMessCore)以 FlowKey 作为 key。FlowKey::Direct 是连接自身的流,FlowKey::Sub 是一个 mux 子流。

generation 字段源于运行时的一条规则:对仍然存活的 key 执行 Effect::Open,会以 RuntimeError::DuplicateKey(open reuses a live outbound key)让连接失败。mux.cool 对端可能在结束一个 session 后立即复用其 id,而此时旧的出站仍在拆除中。因此 Demux 在每接受一个 New 帧时,用 wrapping_add(1) 递增其 generation: u32 计数器,并生成 SubKey { id, generation },使复用的 id 成为一个不同的 key。Demux::session_id 只在该 id 存储的 SubKey 与整个 key 匹配时才接受出站事件,因此发往已退役 generation 的下行字节不会产生任何帧。

本 crate 中的服务端 core 及其 key:

Core 模块 Key TransportAddr BUF_SIZE 使用 Timing
PassthroughCore core Single () 8 KiB 是
HttpCore http Single () MAX_HEAD(64 KiB) 是
TrojanCore trojan FlowKey () 16 KiB 是
VlessCore vless FlowKey () 16 KiB 是
VMessCore vmess FlowKey () 32 KiB 是
ShadowsocksCore ss_legacy Single () 20 KiB 是
Ss2022Core ss_2022 Single () 32 KiB 是
Hy2StreamCore hysteria::server Single () 8 KiB 是
Hy2UdpCore hysteria::server u32(session id) () 16 KiB 否,它自己定期清扫
TunUdpCore tun Single SocketAddr 8 KiB 是
pub enum Phase { Handshake, Sniff, Relay, Closing }
pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
pub const RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
pub enum Expired { Handshake, Sniff, Idle }
pub struct Timing { phase: Phase, armed: bool }
impl Timing {
pub fn new() -> Self;
pub fn phase(&self) -> Phase;
pub fn is_established(&self) -> bool;
pub fn touch<C: ProxyCoreDecode>(&mut self, fx: &mut Effects<'_, C>);
pub fn enter<C: ProxyCoreDecode>(&mut self, phase: Phase, fx: &mut Effects<'_, C>);
pub fn expired<C: ProxyCoreDecode>(&mut self, fx: &mut Effects<'_, C>) -> Expired;
}
pub fn handshake_timed_out() -> io::Error;

运行时只有一个截止时间定时器,由 Effect::SetDeadline(Option<Duration>) 驱动,因此 Timing 让当前阶段决定这个截止时间的含义。每次设置都会推送 Effect::SetDeadline(Some(after)),再次设置会替换之前的截止时间。Timing 实现了 Copy 和 Default,Default 即 new()。

阶段 enter(phase) 设置 touch expired 返回 core 的处理
Handshake HANDSHAKE_TIMEOUT(10 s) 仅在尚未设置时设置 HANDSHAKE_TIMEOUT Expired::Handshake 返回 Err(handshake_timed_out())
Sniff SNIFF_TIMEOUT(300 ms) 无操作 Expired::Sniff 用已收集的内容打开流
Relay RELAY_IDLE_TIMEOUT(300 s) 重新设置 RELAY_IDLE_TIMEOUT Expired::Idle,此前已推送 Effect::Finish 并转入 Closing 返回 Ok(0)
Closing RELAY_IDLE_TIMEOUT 重新设置 RELAY_IDLE_TIMEOUT 同 Relay 返回 Ok(0)

expired 还会清除 armed。is_established() 在 Relay 和 Closing 中为 true。core 在请求解析完成、不再需要客户端提供任何内容时进入 Relay:对单个 TCP 流,就是在推送 Open 的那一刻,早于拨号完成;UDP 关联或 mux.cool 承载连接则在任何 Open 之前进入。Sniff 期间它为 false。每个持有 Timing 的 core 都把它作为自己的 is_established() 转发出去;Hy2UdpCore 没有握手,始终返回 true。应用据此区分从未完成请求的客户端和正在被服务的客户端。

截止时间的设置与重新设置:

  • 握手截止时间只设置一次,由第一次 touch 设置。Timing::new 以 Handshake 阶段、未设置任何截止时间开始,之后在 Handshake 中的 touch 调用都不做任何事。因此时限从客户端的首批字节开始计算,缓慢地逐字节发送也不会延长它。handshake_timed_out() 是 kind 为 TimedOut 的 io::Error,文本为 client did not complete its request in time。
  • 嗅探窗口是固定的。 enter(Phase::Sniff) 设置 SNIFF_TIMEOUT,touch 不会改动它,因此嗅探期间到达的字节不会延长窗口。
  • 空闲截止时间在每个字节事件上重新设置。 core 在 Event::Transport 和 Event::Outbound 的开头调用 touch,承载 UDP 时还在 Event::Datagram 的开头调用。TunUdpCore 的传输层是数据报 socket,它在 Event::TransportDatagram 和 Event::Datagram 上调用。EOF、连接和错误事件不调用它。正因如此,RELAY_IDLE_TIMEOUT 是空闲时限,而不是生命周期上限。
  • core 只会主动进入 Sniff 和 Relay。 Closing 只能通过空闲超时到达。

客户端发送数据之前,运行时不会投递任何事件,所以仅靠 Timing 无法发现连上后一言不发的客户端。core 之上的驱动用同一个常量覆盖这种情况:

驱动 看门狗 超时时的错误
app/src/serve.rs → drive 在 core 尚未建立时,把每次 runtime.next() 包在 tokio::time::timeout(HANDSHAKE_TIMEOUT, …) 中 inbound handshake timed out after 10s
tun/inbound.rs → serve_stream 在 PassthroughCore 运行时上的相同循环 tun: the client never spoke
socks/server.rs → SocksInbound 把整个 SOCKS 握手包在 HANDSHAKE_TIMEOUT 中;之后的中继使用自己的 RELAY_IDLE_TIMEOUT sleep handshake_timed_out()

drive 带有 Established 约束,app/src/serve.rs 中的 established! 宏为 HttpCore、TrojanCore、VlessCore、VMessCore、ShadowsocksCore 和 Ss2022Core 实现了它。见入站服务。

stateDiagram-v2
  [*] --> Handshake: Timing new,未设置
  Handshake --> Handshake: 首次 touch 设置 HANDSHAKE_TIMEOUT
  Handshake --> Sniff: enter Sniff,目标为 IP 且开启嗅探
  Handshake --> Relay: enter Relay
  Sniff --> Relay: Found、Exhausted、EOF 或 Expired Sniff
  Relay --> Relay: touch 重新设置 RELAY_IDLE_TIMEOUT
  Relay --> Closing: Expired Idle 推送 Finish
  Handshake --> [*]: Expired Handshake,core 返回错误
  Closing --> [*]
pub struct SniffPrefix { collector: Collector }
impl SniffPrefix {
pub fn new() -> Self;
pub fn push(&mut self, plain: &[u8]) -> (usize, Verdict);
pub fn held(&self) -> &[u8];
pub fn found(&self) -> Option<&SniffedBehavior>;
pub fn result(&self) -> Option<SniffedBehavior>;
pub fn clear(&mut self);
pub fn is_empty(&self) -> bool;
}

SniffPrefix 包装了 sniff::collector::Collector,这是一个收集流首批明文字节的有界累加器,自身没有时钟。push 最多取 Collector::remaining() 个字节。无论这些字节分几次事件到达,总预算都是 SNIFF_LIMIT(4 KiB)。push 返回它取走的字节数和一个 Verdict:

Verdict 含义 core 的处理
Found 某个嗅探器识别出了 TLS SNI 或 HTTP Host。 立即打开流。
More 尚未识别,预算仍有剩余。 消费返回的字节数并等待。
Exhausted 预算耗尽仍未匹配。 不带嗅探域名打开流。

调用方必须恰好消费返回的字节数。预算耗尽后,push 取 0 个字节,并一直返回 Exhausted。

字节被复制进 collector,因此在流打开之前,它们一直留在 core 的 held 缓冲区中(ProxyCoreDecode::held 返回 prefix.held())。随后 core 推送带 flow.sniffed = prefix.result() 的 Effect::Open,若持有字节,再推送 Effect::ForwardHeld { range: 0..held.len() },并在下一个字节事件的开头调用 clear()。这是安全的,依据是运行时的 held 缓冲区规则:只要还有 held 转发在排队,运行时就不投递字节事件,所以在字节事件开始时,之前所有的 held 范围都已应用。result() 克隆嗅探结果,并把字节留在原处供那次转发使用。

sniff/mod.rs 中的 worth_sniffing(&destination) 仅对 IP 目标为 true。已经指明域名的流会按该域名路由,因此 core 对它跳过嗅探阶段,不会为它花费最多 SNIFF_TIMEOUT 的时间。

pub struct Passthrough<K> { key: K, outbound_eof: bool, transport_eof: bool }
impl<K: Copy> Passthrough<K> {
pub fn new(key: K) -> Self;
pub fn key(&self) -> K;
pub fn is_done(&self) -> bool;
pub fn on_transport<C: ProxyCoreDecode<Key = K>>(&self, data: &[u8], fx: &mut Effects<'_, C>) -> usize;
pub fn on_outbound<C: ProxyCoreDecode<Key = K>>(&self, data: &[u8], fx: &mut Effects<'_, C>) -> io::Result<usize>;
pub fn on_outbound_eof<C: ProxyCoreDecode<Key = K>>(&mut self, fx: &mut Effects<'_, C>);
pub fn on_transport_eof<C: ProxyCoreDecode<Key = K>>(&mut self, fx: &mut Effects<'_, C>);
pub fn on_outbound_gone<C: ProxyCoreDecode<Key = K>>(&mut self, fx: &mut Effects<'_, C>);
}
pub fn staging_full() -> io::Error;

Passthrough 是只有一个流式出站的协议的中继尾段。大多数协议会对传输层一侧做分帧或加密,这类 core 自行解码传输层字节,只使用其中的半关闭记录。on_transport 和 on_outbound 是原样转发的形式,供明文中继使用:PassthroughCore、头部之后的 Trojan 和 VLESS、HTTP CONNECT 隧道,以及 Hysteria 2 的代理流。

方法 状态变化 Effect
on_transport(data) 无 Forward { key, range: 0..data.len() };返回 data.len()
on_outbound(data) 无 把 data 暂存到发往传输层的方向;返回 data.len() 或 Err(staging_full())
on_outbound_eof outbound_eof = true ShutdownTransport,若传输层已结束则再 Finish
on_transport_eof transport_eof = true Shutdown { key },若出站已结束则再 Finish
on_outbound_gone 两个标志都置位 ShutdownTransport、Finish

两个标志都置位后 is_done() 为 true。on_outbound_gone 处理连接失败或出站出错:此时已无数据可以流动,core 结束连接,但已经暂存到传输层方向的字节仍会写出。staging_full() 是 io::Error::other("staging room below the core's declared reserve")。出现它说明 core 暂存的数据超过了其 STAGING_RESERVE 的承诺。这是 core 的 bug 而不是对端错误,因为运行时只在暂存区能容纳保留量加负载时才轮询出站。

Passthrough 是 Copy 的,字节相关方法接受 &self。core 在调用这些方法之前先把它从状态枚举中复制出来(let relay = *relay;),从而把对 self.state 的借用与随后的 self.prefix.clear() 调用分开。

pub struct PassthroughCore<T> {
pending: Option<Flow<T>>,
sniff: bool,
prefix: SniffPrefix,
relay: Passthrough<Single>,
timing: Timing,
}
impl<T> PassthroughCore<T> {
pub const BUF_SIZE: usize = 8 * 1024;
pub fn new(flow: Flow<T>) -> Self;
pub fn sniffing(flow: Flow<T>) -> Self;
pub fn is_established(&self) -> bool;
}
impl<T: Send + Sync + 'static> ProxyCoreDecode for PassthroughCore<T> {
type Key = Single;
type Target = Flow<T>;
type Error = io::Error;
type TransportAddr = ();
const STAGING_RESERVE: usize = 0;
fn handle(&mut self, event: Event<'_, Self>, fx: &mut Effects<'_, Self>) -> Result<usize, io::Error>;
fn held(&self) -> &[u8];
}

最小的完整 core。它服务于在第一个字节之前就已知目标的流,并在两个方向上原样中继。TUN 入站对每条 TCP 连接都使用它,因为 IP 栈已经告诉它连接要去哪里。sniffing(flow) 只在 worth_sniffing 判定目标为 IP 时才开启嗅探阶段。PassthroughCore 也是展示各部件如何组合的参考实现:

事件 处理
待打开且正在嗅探时的 Transport 第一次时进入 Sniff,调用 prefix.push,在结论不是 More 时打开。返回取走的字节数。
其他情况下的 Transport 若尚未打开则打开,然后 touch、prefix.clear()、relay.on_transport。
Outbound touch、prefix.clear()、relay.on_outbound。
TransportEof 若仍待打开,用已嗅探的结果打开,然后 relay.on_transport_eof。
OutboundEof relay.on_outbound_eof。
ConnectFailed、OutboundError 丢弃待打开的流,然后 relay.on_outbound_gone。
Deadline Expired::Handshake 返回 Err(handshake_timed_out()),Expired::Sniff 打开流,Expired::Idle 返回 Ok(0)。
Connected、数据报事件 忽略。

打开时推送 Effect::Open { key: Single, target: flow },held 前缀非空时再推送覆盖它的 Effect::ForwardHeld,然后进入 Relay。不嗅探的 core 在第一个 Transport 事件上打开,因此该事件产生的 effect 依次为:Open、来自 enter 的 SetDeadline(RELAY_IDLE_TIMEOUT)、来自 touch 的 SetDeadline(RELAY_IDLE_TIMEOUT),以及 Forward。

pub const HARNESS_STAGING: usize = 64 * 1024;
pub struct CoreHarness<C: ProxyCoreDecode> {
pub core: C,
effects: EffectList<C>,
staging: WriteBuffer<HARNESS_STAGING>,
packets: Option<PacketList<C::TransportAddr>>,
}
impl<C: ProxyCoreDecode> CoreHarness<C> {
pub fn new(core: C) -> Self;
pub fn over_datagrams(core: C) -> Self;
pub fn event(&mut self, event: Event<'_, C>) -> Result<(usize, Vec<Effect<C>>), C::Error>;
pub fn transport(&mut self, data: &mut [u8]) -> Result<(usize, Vec<Effect<C>>), C::Error>;
pub fn feed(&mut self, data: &mut [u8]) -> Result<(usize, Vec<Effect<C>>), C::Error>;
pub fn outbound(&mut self, key: C::Key, data: &mut [u8]) -> Result<(usize, Vec<Effect<C>>), C::Error>;
pub fn staged(&mut self) -> Vec<u8>;
pub fn staged_packets(&mut self) -> Vec<(Vec<u8>, C::TransportAddr)>;
pub fn held(&self, range: std::ops::Range<usize>) -> Vec<u8>;
}

运行时的替身,由测试手动驱动,各 core 的单元测试都使用它。它不做 I/O,也没有时钟,所以测试要自己投递 Event::Deadline。

  • event 投递一个事件,返回消费的字节数和本次调用中推送的 effect。
  • transport 投递一个 Event::Transport。feed 会像运行时那样,在 core 仍有进展时对未消费的剩余部分反复调用它,直到 core 消费 0 个字节为止。它会平移 Forward 和 SendTo 的范围,使其相对于你传入的切片是绝对位置。ForwardHeld 和 SendToHeld 的范围索引的是 held 缓冲区,保持不变;用 held(range) 解析它们。
  • outbound 为某个 key 投递 Event::Outbound。
  • staged 取走到目前为止暂存到传输层方向的全部数据。暂存的字节在 harness 的 HARNESS_STAGING(64 KiB)缓冲区中累积,直到测试取走。
  • over_datagrams 为数据报传输层构造 harness,使 Effects::stage_to 可用。staged_packets 按顺序返回每个暂存的包及其对端。

harness 不执行运行时的调度规则:暂存保留量检查、held 缓冲区固定、FrameTooLarge、DuplicateKey 以及其他 RuntimeError 检查。这些由 pipeline 测试覆盖,它们在真实运行时下运行每个 core。

典型的单流 core 把 Timing 和 SniffPrefix 作为字段,通过一个状态枚举推进,开始中继后状态中持有一个 Passthrough。TrojanCore 很适合拿来阅读。对于开启嗅探、目标为 IP 的 CONNECT,交互过程如下:

sequenceDiagram
  participant R as 运行时
  participant C as Core
  participant P as SniffPrefix
  R->>C: Event Transport,携带请求头
  C->>R: 来自 touch 的 SetDeadline HANDSHAKE_TIMEOUT
  C->>C: 解析请求头,查找用户,Flow new
  C->>R: 来自 enter Sniff 的 SetDeadline SNIFF_TIMEOUT
  R->>C: Event Transport,携带首段负载
  C->>P: push 负载
  P-->>C: 取走的字节数和 Verdict Found
  C->>R: 用嗅探后的流 Open
  C->>R: 对 held 前缀 ForwardHeld
  C->>R: 来自 enter Relay 的 SetDeadline RELAY_IDLE_TIMEOUT
  R->>R: 拨号,然后应用 held 转发
  R->>C: Event Outbound,携带回复字节
  C->>R: 来自 touch 的 SetDeadline RELAY_IDLE_TIMEOUT
  C->>C: prefix clear
  C->>R: 把回复暂存到传输层方向

如果嗅探截止时间先到,Event::Deadline 得到 Expired::Sniff,core 以 sniffed = None 和已持有的字节打开流。如果目标地址是域名,或嗅探关闭,core 在解析完请求头后立即打开。UDP 关联和 mux.cool 承载连接跳过 Sniff,直接进入 Relay。

pub struct AddressCodec {
pub ipv4: u8,
pub domain: u8,
pub ipv6: u8,
pub port_first: bool,
}
impl AddressCodec {
pub const SOCKS: Self;
pub const VMESS: Self;
pub const MAX_LEN: usize = 1 + 1 + 255 + 2;
pub fn encoded_len(remote: &Remote) -> usize;
pub fn write_slice(&self, out: &mut [u8], remote: &Remote, port: u16) -> Option<usize>;
pub async fn read<R: AsyncRead + Unpin>(&self, r: &mut R) -> io::Result<(Remote, u16)>;
pub async fn read_destination<R: AsyncRead + Unpin>(&self, r: &mut R, network: DialNetwork) -> io::Result<Destination>;
pub fn read_slice(&self, data: &[u8]) -> io::Result<(Remote, u16, usize)>;
pub fn write_buf(&self, out: &mut BytesMut, remote: &Remote, port: u16);
pub async fn write<W: AsyncWrite + Unpin>(&self, w: &mut W, remote: &Remote, port: u16) -> io::Result<()>;
}

移植自 Xray 的 AddressParser。一个地址由类型字节、地址字节和大端序端口组成。各协议的差别只在三个类型值以及端口是否在前,所以一个 codec 就是这四个字段。

AddressCodec::SOCKS,端口在后:

字段 长度 含义
type 1 0x01 IPv4,0x03 域名,0x04 IPv6
address 4、16 或 1 + n IPv4 字节;IPv6 字节;或一个长度字节 n 后跟 n 个域名字节
port 2 大端序

AddressCodec::VMESS,端口在前:

字段 长度 含义
port 2 大端序
type 1 0x01 IPv4,0x02 域名,0x03 IPv6
address 4、16 或 1 + n 同上
布局 使用方
SOCKS SOCKS5(socks/protocol.rs)、Trojan(trojan/protocol.rs)、Shadowsocks(ss_legacy/protocol.rs)、Shadowsocks 2022(ss_2022/protocol.rs)
VMESS VLESS(vless/protocol.rs)、mux.cool 帧(mux/frame.rs)、VMess(vmess/protocol.rs)

除 VMess 外,每个模块都用 pub const ADDR: AddressCodec = …; 声明一次自己的布局。VMess 在两处调用点直接写 AddressCodec::VMESS。

encoded_len 计入类型字节和端口:IPv4 为 7 字节,IPv6 为 19 字节,n 字节的域名为 n + 4 字节。MAX_LEN 为 259,即域名为 255 字节时的长度。协议用它确定固定头部缓冲区的大小,例如 Shadowsocks 客户端的 STAGING_RESERVE。

切片形式和异步形式共用以下解码规则:

  • 未知类型字节为 InvalidData,文本为 unknown address type: <n>。
  • 域名必须是非空 UTF-8,且只包含 ASCII 字母、数字、-、. 和 _,否则为 InvalidData(empty domain name、non-utf8 domain、invalid domain name: …)。
  • 以数字或 [ 开头、且(去掉方括号后)能解析为 IP 的域名会变成 Remote::IpAddr,与 Xray 的 maybeIPPrefix 做法相同。
  • 在 read_slice 中,输入不足是 Truncated 错误,到达调用方时为 UnexpectedEof,因此调用方可以用 need_more 包装它。返回的计数是消费的字节数,包括端口。异步的 read 则返回底层 read_exact 的 UnexpectedEof。

write_slice 写入固定大小的切片,在切片太短或域名超过 255 字节时返回 None。core 用这种形式把数据封装进暂存空间。write_buf 追加到 BytesMut,用于输出会增长的场景;与 write_slice 不同,它不检查 255 字节的域名上限,调用方必须自行检查。

pub fn format_authority(dest: &Destination) -> String;
pub fn parse_authority(raw: &str, default_port: u16) -> io::Result<Destination>;

这两个函数服务于以文本形式携带目标的协议:HTTP 代理服务端,用于 CONNECT 目标和普通代理请求的主机(http/core.rs);HTTP 客户端的 CONNECT 请求行(http/protocol.rs → build_connect_request);以及 Hysteria 2 的 TCP 请求和 UDP 消息(hysteria/connector.rs、hysteria/server/inbound.rs、hysteria/server/datagrams.rs)。

  • format_authority 为 IPv6 加方括号,如 [2001:db8::1]:443。
  • parse_authority 会去掉输入两端的空白,接受 host、host:port、[v6] 和 [v6]:port。端口缺省时使用 default_port,不解析域名,返回 TCP Destination。
  • parse_authority 拒绝空主机(empty authority host)、不带方括号的 IPv6(ambiguous authority (bracket IPv6 literals))、无效端口(invalid port)、未闭合的方括号(malformed IPv6 authority),以及方括号字面量之后的多余数据(trailing data after IPv6 authority)。这些都是 InvalidData。

helpers/address_family.rs 决定出站可以使用目标解析出的哪些 IP。它回答两个相互独立的问题:

pub enum AddressFamilyStrategy { Auto, Ipv4Only, Ipv6Only, PreferIpv4, PreferIpv6 }
pub struct FamilySupport { ipv4: bool, ipv6: bool }
pub async fn resolve_candidates(
context: &str,
dest: &Destination,
strategy: AddressFamilyStrategy,
support: FamilySupport,
resolver: &Resolver,
) -> io::Result<Vec<IpAddr>>;
pub fn select_candidate_ips(
resolved: Vec<IpAddr>,
strategy: AddressFamilyStrategy,
support: FamilySupport,
) -> Vec<IpAddr>;
pub async fn destination_to_socketaddrs(
dest: &Destination,
strategy: AddressFamilyStrategy,
resolver: &Resolver,
) -> io::Result<Vec<SocketAddr>>;
  • 策略(AddressFamilyStrategy,默认 Auto)是运维人员的要求。它的 FromStr 比较宽松:去掉两端空白、转为小写、把 - 映射为 _,并接受 ipv4、v4、4、ipv4only、prefer_v6 和 v6_prefer 等别名。空字符串为 Auto,其他值为 AddressFamilyStrategyParseError(unknown address family strategy)。
  • 能力(FamilySupport)是出站能以哪些地址族作为源地址发送流量。把源地址选择交给内核的拨号器传入 FamilySupport::both(),对不可用的地址族,内核会以 ENETUNREACH 快速失败。WireGuard 运行的是没有路由表的用户态网络栈,因此它用 FamilySupport::from_addrs 从隧道地址推导能力。

select_candidate_ips 按解析器返回的顺序保留所有通过两项检查的地址。Prefer* 策略随后做一次稳定排序,使另一个地址族作为后备保留,而不是被丢弃。保留全部候选而不只是第一个,正是拨号器能在首个地址不可达时继续尝试的原因。解析结果为空的域名为 NotFound,文本为 <context>: destination did not resolve。过滤后一个都不剩时,no_candidate_error 返回 AddrNotAvailable,文本为 <context>: no usable <strategy> destination address for <host>:<port>。只要调用方的 FamilySupport 不是两个地址族都支持,它就会追加 (local address supports …),因此由内核路由的拨号器永远不会得到这段说明。

transports/connect.rs、hysteria/connection.rs、app/src/outbound/freedom.rs 和 app/src/balancer.rs 通过 destination_to_socketaddrs 解析,其 context 为 dial。wireguard/connector.rs 用自己的 FamilySupport 调用 resolve_candidates。见拨号器。

pub fn take<'a, I>(data: &'a [u8], index: I, what: &'static str) -> Result<&'a [u8], ProtocolError>
where
I: SliceIndex<[u8], Output = [u8]>;
pub fn take_array<const N: usize>(data: &[u8], at: usize) -> Result<[u8; N], ProtocolError>;
pub fn need_more<T>(result: std::io::Result<T>) -> std::io::Result<Option<T>>;

helpers/parse.rs 中的这三个函数取代了被 crate 级 lint 禁止的 slice[a..b] 和 slice[a..b].try_into().unwrap():

  • take 以任意范围形式(..b、a..、a..b、..)借出子切片,并把越界访问映射为 Truncated(what)。
  • take_array::<N> 从偏移 at 复制 N 个字节。它用 checked_add 计算结束偏移(溢出为 Overflow("field offset")),并检查范围边界(输入不足为 Truncated("fixed-size field"))。用于 u16::from_be_bytes(take_array::<2>(data, at)?) 这样的读取。
  • need_more 用于接收不断增长的缓冲区、并在更多字节到达时被再次调用的切片解析器。它把 UnexpectedEof 错误变为 Ok(None),其他错误保持为错误。

由此得到的头部解析器形态是 fn parse(buf: &[u8]) -> io::Result<Option<(Header, usize)>>:

返回值 对 core 的含义
Ok(None) 字节还不够。消费 0 个字节,等待下一个 Event::Transport。
Ok(Some((header, used))) 完整的头部。消费 used 个字节。
Err(e) 对端说的不是这个协议。让连接失败。

trojan/protocol.rs → parse_request_header 展示了这种写法:

let hash = match need_more(take_array::<HASH_LEN>(buf, 0).map_err(io::Error::from))? {
Some(hash) => hash,
None => return Ok(None),
};
let Some(crlf) = need_more(take_array::<2>(buf, HASH_LEN).map_err(io::Error::from))? else {
return Ok(None);
};
if crlf != CRLF {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"trojan: not trojan protocol (missing CRLF after hash)",
));
}

need_more 只能用于切片解析的结果。真实读取产生的 UnexpectedEof 表示对端已关闭,need_more 会把它变成等待。从线上读到的长度要用 checked_add 或 saturating_add 计算,固定字段(版本、保留字节、分隔符、命令)要显式检查:错误的分隔符必须是错误,而不是等待。

byte_newtype!(protocols/src/macros.rs,在 crate 根导出)声明一个基于 [u8; N] 的 Clone + Copy newtype,并带有 pub const fn as_bytes(&self) -> &[u8; N]。vmess/aead.rs 用它定义 GcmKey(16 字节)、GcmNonce(12 字节)和 ConnNonce(8 字节),使不同用途的值不会在调用点混用。以用途命名的构造函数把长度相同的值区分开。

pub fn evp_bytes_to_key(password: &[u8], key_len: usize) -> Vec<u8>;
pub fn hkdf_sha1_ss_subkey(master_key: &[u8], salt: &[u8], out: &mut [u8]);
pub fn increment_le(nonce: &mut [u8]);
pub fn ct_eq(a: &[u8], b: &[u8]) -> bool;
函数 计算内容 调用方
evp_bytes_to_key 使用 MD5、不加盐的 OpenSSL EVP_BytesToKey:D_i = MD5(D_(i-1) ‖ password),拼接后截断到 key_len。Shadowsocks 用它从密码派生主密钥。 ss_legacy/users.rs;app 的 Shadowsocks 出站(app/src/outbound/mod.rs)和 katana 的 Shadowsocks 出站(src/outbound/mod.rs)
hkdf_sha1_ss_subkey 以盐为 salt、主密钥为输入密钥材料、ss-subkey 为 info 字符串的 HKDF-SHA1,填满 out。这是 Shadowsocks AEAD 的每会话子密钥。 ss_legacy/aead.rs
increment_le 原地把小端序计数器加一,跨字节进位,到顶回绕。 ss_legacy/aead.rs 和 ss_2022/crypto.rs 中的分块 nonce
ct_eq 通过 subtle::ConstantTimeEq 做常量时间比较。长度不同的切片视为不相等。 trojan/users.rs → Validator::get;ss_2022/protocol.rs 和 ss_2022/codec.rs 中的响应盐检查;hysteria/server/authenticator.rs

对于 16 或 32 字节的 Shadowsocks 子密钥,hkdf_sha1_ss_subkey 不会失败:HKDF-SHA1 只在输出超过 255 × 20 字节时才失败。在这条不可达路径上,由于 crate lint 的要求,它把 out 填零而不是 panic。

不变量 机制 由谁保证
本 crate 的生产代码不会因输入而 panic crate 级 deny unwrap_used、expect_used、indexing_slicing 和 arithmetic_side_effects;take、take_array 和检查过的算术 cargo clippy --workspace --all-targets --all-features -- -D warnings
不完整的切片等待,格式错误的切片失败 Truncated 映射为 UnexpectedEof,need_more 只把这个 kind 映射为 None protocols/tests/unit/trojan/protocol.rs 和 protocols/tests/unit/vless/protocol.rs 中的 request_header_is_parsed_from_a_slice_once_whole
握手截止时间只设置一次,从首批字节开始计算 Timing.armed;Handshake 中的 touch protocols/tests/unit/core/mod.rs 中的 timing_arms_handshake_once_then_idle_per_byte_event
中继截止时间是空闲时限 touch 在每个字节事件上重新设置 RELAY_IDLE_TIMEOUT 同一个测试
空闲超时会结束连接 Timing::expired 推送 Effect::Finish 并转入 Closing 同一个测试;passthrough_core_finishes_when_the_outbound_fails_or_idles
握手和嗅探超时交由 core 处理 expired 对它们不推送任何 effect timing_reports_handshake_and_sniff_expiry_to_the_core
嗅探持有的字节永不超过 SNIFF_LIMIT SniffPrefix::push 最多取 Collector::remaining() sniff_prefix_takes_no_more_than_its_budget
嗅探的字节会被转发而不会丢失 先 Open 再对 held 前缀 ForwardHeld;只在下一个字节事件时 clear sniff_prefix_finds_a_host_across_pushes_and_keeps_the_bytes、a_sniffing_passthrough_core_holds_the_prefix_and_opens_with_the_host、a_sniffing_passthrough_core_opens_on_the_sniff_deadline
按域名寻址的流从不因嗅探而被扣留 worth_sniffing a_sniffing_passthrough_core_with_a_domain_target_opens_at_once
中继只在两个方向都关闭后才结束 Passthrough 的两个标志 passthrough_half_closes_each_side_and_finishes_on_the_second
复用的 mux session id 不会与仍在拆除的出站冲突 SubKey.generation,每个 New 递增 protocols/tests/unit/mux/demux.rs 中的 stream_sessions_open_forward_and_end_with_fresh_generations
编码后的地址可往返还原,且 encoded_len 精确 AddressCodec protocols/tests/unit/helpers/address.rs 中的 write_slice_matches_write_buf_and_bounds_itself
Prefer* 把另一个地址族保留为后备 select_candidate_ips 中的稳定排序 protocols/tests/unit/helpers/address_family.rs 中的 prefer_ipv4_keeps_ipv6_as_fallback
网络栈出站永远不会拿到它无法作为源的地址 FamilySupport::from_addrs auto_skips_families_without_a_local_address
  • 握手超时。 遇到 Expired::Handshake 时,core 返回 Err(handshake_timed_out())(TimedOut),运行时结束连接。完全不发数据的客户端则由驱动的看门狗结束(见 Phase、Timing 与 Expired 下的表格)。
  • 未知用户。 Trojan、VLESS、VMess 和 Shadowsocks 返回 kind 为 PermissionDenied 的 io::Error,Shadowsocks 2022 返回 InvalidData;连接不作任何回复即结束。HTTP core 则暂存一个 407 响应,然后 ShutdownTransport 并 Finish。
  • 帧格式错误。 解析器直接返回 InvalidData,或返回转换后的 ProtocolError,该错误会结束连接。sniff 中的任何东西都不会让连接失败:无法识别的字节只是得不到嗅探域名。
  • 出站失败。 ConnectFailed 或 OutboundError 触发 Passthrough::on_outbound_gone,推送 ShutdownTransport 和 Finish。已暂存的字节仍会写出。
  • 暂存溢出。 staging_full() 让连接失败。它表示 core 的 STAGING_RESERVE 声明有误,而不是对端恶意。
  • 取消。 core 不拥有任何 task、定时器或 socket,所以 drop 一个 core 总是安全的。取消发生在更上层:app 在其 generation(一代实例)的 CancellationToken 下 spawn 每条连接(见 Generation 与热重载),drop 运行时即 drop core。
常量 值 定义位置
HANDSHAKE_TIMEOUT 10 s protocols/src/core/mod.rs
RELAY_IDLE_TIMEOUT 300 s protocols/src/core/mod.rs
SNIFF_TIMEOUT 300 ms protocols/src/sniff/mod.rs
SNIFF_LIMIT 4 KiB(4096 字节) protocols/src/sniff/mod.rs
PassthroughCore::BUF_SIZE 8 KiB protocols/src/core/mod.rs
PassthroughCore::STAGING_RESERVE 0 protocols/src/core/mod.rs
HARNESS_STAGING 64 KiB protocols/src/core/harness.rs
AddressCodec::MAX_LEN 259 字节 protocols/src/helpers/address.rs
AddressCodec 能编码的最长域名 255 字节(一个长度字节) protocols/src/helpers/address.rs

大多数模块采用相同的拆分方式。并非每个模块都有每个文件,模块也会为自身特有的内容增加文件(VMess 增加了 aead.rs、keys.rs、framing.rs 和 session.rs;Shadowsocks 增加了 aead.rs 或 crypto.rs)。

文件 内容 示例
mod.rs 模块文档和公开的 re-export trojan/mod.rs:pub use core::TrojanCore;
protocol.rs 线格式原语:常量、模块的 ADDR codec、返回 io::Result<Option<(T, usize)>> 的切片解析器、编码器,以及测试使用的异步读写形式 trojan/protocol.rs → parse_request_header、encode_request_header
core.rs 服务端 core:状态枚举、Timing、SniffPrefix、中继时的 Passthrough、BUF_SIZE、STAGING_RESERVE、is_established TrojanCore、VlessCore、VMessCore、ShadowsocksCore、Ss2022Core、HttpCore
codec.rs 实现 ProxyCoreEncodeHandshake 加 ProxyCoreEncode(流)或 ProxyCoreEncodeDatagram(UDP)的客户端 codec TrojanStream、TrojanDatagram、VlessStream、SsStream、HttpConnect
config.rs 服务端配置类型 VlessServerConfig、HttpServerConfig、SocksServerConfig
users.rs、validator.rs、accounts.rs 用户表,对载荷 T 泛型,发放 Arc<T>。core 通过 Arc 持有它们。有些模块也把服务端配置放在这里(TrojanServerConfig、ShadowsocksServerConfig、Ss2022ServerConfig)。 trojan::Validator、vless::Validator、vmess::AccountValidator

例外都是结构性的:

  • SOCKS 由自己的驱动(socks/server.rs → SocksInbound)服务,而不是由 core 服务。
  • Hysteria 2 拥有自己的 QUIC endpoint,每条代理流运行一个运行时(Hy2StreamCore),每条连接的数据报运行一个运行时(Hy2UdpCore)。
  • TUN 拥有自己的设备,每条 TCP 连接运行一个运行时(PassthroughCore),每个客户端源地址的 UDP 运行一个运行时(TunUdpCore)。
  • WireGuard 只有出站 connector。

单元测试位于 src/ 之外的 protocols/tests/unit/<module>/<file>.rs。它们被编译进所测试的模块,因此可以访问私有项:

#[cfg(test)]
#[path = "../../tests/unit/trojan/core.rs"]
mod tests;

pipeline 测试在真实运行时下让服务端 core 与客户端 codec 相互对接。它们是 protocols/tests/pipeline.rs 的子模块,每个协议一个文件,位于 protocols/tests/pipeline/ 下。hysteria 和 tun 模块与所测代码受相同的 feature 控制。

  1. 编写线格式原语,放在 protocols/src/<name>/protocol.rs。用 take、take_array 和检查过的算术从切片解析;字节不足时通过 need_more 返回 Ok(None);显式检查每个固定字段(版本、保留字节、长度、命令)。地址布局相符时复用 AddressCodec::SOCKS 或 AddressCodec::VMESS,否则声明一个新的 AddressCodec 值。把结果放进 pub const ADDR。

  2. 编写服务端 core,放在 core.rs。实现 ProxyCoreDecode,其中 Target = Flow<T>、Error = io::Error、Key = Single(能承载 mux.cool 时用 FlowKey),流式传输层用 TransportAddr = ()。在每个字节事件开头调用 timing.touch(fx),请求解析完成后调用 enter(Phase::Sniff) 或 enter(Phase::Relay),并处理全部三种 Expired。把 BUF_SIZE 声明为协议允许的最大帧,STAGING_RESERVE 要覆盖一次调用在负载之外暂存的全部内容,并公开 is_established()。

  3. 编写客户端 codec,放在 codec.rs,基于 ProxyCoreEncodeHandshake 加 ProxyCoreEncode;如果协议承载 UDP,再实现 ProxyCoreEncodeDatagram。见客户端运行时。

  4. 添加用户和配置,放在 users.rs 或 validator.rs 以及 config.rs 中,对 T 泛型。用 ct_eq 比较密钥并扫描整张表,就像 trojan::Validator::get 那样。绝不记录凭据或密钥。

  5. 注册模块,在 protocols/src/lib.rs 中。如果它带来沉重的依赖树,就像 hysteria 和 tun 那样放到一个新的 Cargo feature 之后,并以同样方式控制其 pipeline 测试模块。

  6. 编写测试。 在 protocols/tests/unit/<name>/ 中编写通过 CoreHarness 驱动 core 的单元测试,包括针对格式错误输入、截断输入和未知用户的负向测试。在 protocols/tests/pipeline/<name>.rs 中添加 pipeline 测试,并在 protocols/tests/pipeline.rs 中注册。

  7. 接入 app。 在 app/src/config.rs 中添加设置结构体,在 app/src/inbound/mod.rs 中添加一个 StreamProtocol 变体及其构建分支,在 app/src/serve.rs 中添加一个 drive::<{ Core::<()>::BUF_SIZE }, _, _> 分支并把 core 的名字加入 established! 列表,在 app/src/outbound/mod.rs 中添加一个 Outbound 变体。见从配置到运行的流水线。

  8. 运行门禁:cargo fmt --all -- --check、cargo test --workspace 和 cargo clippy --workspace --all-targets --all-features -- -D warnings。

测试 文件 覆盖内容
timing_arms_handshake_once_then_idle_per_byte_event protocols/tests/unit/core/mod.rs 握手截止时间只设置一次、每个事件重新设置空闲截止时间,以及空闲超时推送 Finish
timing_reports_handshake_and_sniff_expiry_to_the_core 同上 Expired::Handshake 和 Expired::Sniff 不推送任何 effect
sniff_prefix_finds_a_host_across_pushes_and_keeps_the_bytes 同上 跨两次 push 的 Host 头;held 字节保留到 clear
sniff_prefix_takes_no_more_than_its_budget 同上 SNIFF_LIMIT 上限和 Exhausted
passthrough_half_closes_each_side_and_finishes_on_the_second 同上 半关闭顺序和 Finish
passthrough_core_opens_on_the_first_bytes_and_relays_verbatim 同上 打开、截止时间和转发的顺序
passthrough_core_finishes_when_the_outbound_fails_or_idles 同上 ConnectFailed 和空闲 Deadline
a_sniffing_passthrough_core_holds_the_prefix_and_opens_with_the_host 同上 嗅探、带域名 Open、ForwardHeld,然后 clear
a_sniffing_passthrough_core_opens_on_the_sniff_deadline 同上 在 Expired::Sniff 时不带域名打开
a_sniffing_passthrough_core_with_a_domain_target_opens_at_once 同上 域名目标没有嗅探阶段
passthrough_runtime_relays_a_tcp_flow_and_half_closes protocols/tests/pipeline/core.rs PassthroughCore 在真实运行时下中继 100,000 字节
write_slice_matches_write_buf_and_bounds_itself protocols/tests/unit/helpers/address.rs 两种布局都能往返还原;encoded_len;过短的输出和 256 字节的域名被拒绝
authority_ipv4_with_port、authority_domain_default_port、authority_ipv6_bracketed 同上 parse_authority
parses_address_family_strategy_aliases、auto_skips_families_without_a_local_address、auto_keeps_resolver_order_when_both_families_are_supported、ipv4_only_filters_to_ipv4、prefer_ipv4_keeps_ipv6_as_fallback、a_kernel_routed_dialer_is_limited_by_policy_alone、the_capability_clause_is_omitted_for_a_kernel_routed_dialer protocols/tests/unit/helpers/address_family.rs 别名、过滤、后备顺序和错误文本
request_header_is_parsed_from_a_slice_once_whole protocols/tests/unit/trojan/protocol.rs、protocols/tests/unit/vless/protocol.rs 头部在任意位置截断时 need_more 都返回 None
stream_sessions_open_forward_and_end_with_fresh_generations protocols/tests/unit/mux/demux.rs 每个 New 对应一个新的 SubKey generation
evp_key_known_answer protocols/tests/unit/ss_legacy/aead.rs evp_bytes_to_key 与密码的 MD5 对照
unknown_user_is_refused、unknown_uuid_is_refused、an_unknown_password_is_refused protocols/tests/unit/trojan/core.rs、protocols/tests/unit/vless/core.rs、protocols/tests/unit/ss_legacy/core.rs core 返回的 PermissionDenied