协议 crate 基础
源码文件:47 个 · 核对版本 Etemenanki 596916d · katana v3.0.1
Etemenanki/protocols/Cargo.tomlEtemenanki/protocols/src/lib.rsEtemenanki/protocols/src/flow.rsEtemenanki/protocols/src/error.rsEtemenanki/protocols/src/macros.rsEtemenanki/protocols/src/core/mod.rsEtemenanki/protocols/src/core/harness.rsEtemenanki/protocols/src/http/core.rsEtemenanki/protocols/src/ss_legacy/core.rsEtemenanki/protocols/src/ss_2022/core.rsEtemenanki/protocols/src/vmess/core.rsEtemenanki/protocols/src/tun/udp.rsEtemenanki/protocols/src/helpers/address.rsEtemenanki/protocols/src/helpers/parse.rsEtemenanki/protocols/src/helpers/crypto.rsEtemenanki/protocols/src/helpers/address_family.rsEtemenanki/protocols/src/sniff/mod.rsEtemenanki/protocols/src/sniff/collector.rsEtemenanki/protocols/src/mux/demux.rsEtemenanki/protocols/src/mux/frame.rsEtemenanki/protocols/src/trojan/core.rsEtemenanki/protocols/src/trojan/protocol.rsEtemenanki/protocols/src/trojan/users.rsEtemenanki/protocols/src/vless/protocol.rsEtemenanki/protocols/src/vmess/protocol.rsEtemenanki/protocols/src/vmess/aead.rsEtemenanki/protocols/src/socks/protocol.rsEtemenanki/protocols/src/socks/server.rsEtemenanki/protocols/src/ss_legacy/protocol.rsEtemenanki/protocols/src/ss_2022/protocol.rsEtemenanki/protocols/src/tun/inbound.rsEtemenanki/protocols/tests/pipeline.rsEtemenanki/protocols/tests/pipeline/core.rsEtemenanki/protocols/tests/unit/core/mod.rsEtemenanki/protocols/tests/unit/helpers/address.rsEtemenanki/protocols/tests/unit/helpers/address_family.rsEtemenanki/app/Cargo.tomlEtemenanki/app/src/flow.rsEtemenanki/app/src/router.rsEtemenanki/app/src/serve.rsEtemenanki/app/src/config.rsEtemenanki/app/src/inbound/mod.rsEtemenanki/app/src/outbound/mod.rsEtemenanki/app/src/outbound/udp_fanout.rsEtemenanki/concepts/src/core.rskatana/Cargo.tomlkatana/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,以及最小的完整 corePassthroughCore(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 来记录半关闭状态。
模块一览与 feature 开关
Section titled “模块一览与 feature 开关”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 |
crate 级 lint
Section titled “crate 级 lint”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 的解析中的辅助函数正是因这条规则而存在。
Flow<T>
Section titled “Flow<T>”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.coolNew帧对应一个子流。当入站开启嗅探且子流目标是 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 的按用户流量计数器,它必须始终是唯一的共享实例。
ProtocolError
Section titled “ProtocolError”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 响应,而不是返回错误。
FlowKey 与 SubKey
Section titled “FlowKey 与 SubKey”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 | 是 |
Phase、Timing 与 Expired
Section titled “Phase、Timing 与 Expired”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 --> [*]
SniffPrefix
Section titled “SniffPrefix”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 的时间。
Passthrough<K>
Section titled “Passthrough<K>”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() 调用分开。
PassthroughCore<T>
Section titled “PassthroughCore<T>”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。
CoreHarness<C>
Section titled “CoreHarness<C>”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。
AddressCodec
Section titled “AddressCodec”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 字节的域名上限,调用方必须自行检查。
文本 authority
Section titled “文本 authority”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,不解析域名,返回 TCPDestination。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。见拨号器。
不会 panic 的解析
Section titled “不会 panic 的解析”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 字节),使不同用途的值不会在调用点混用。以用途命名的构造函数把长度相同的值区分开。
加密辅助函数
Section titled “加密辅助函数”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 |
失败路径与取消
Section titled “失败路径与取消”- 握手超时。 遇到
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 |
协议模块的目录布局
Section titled “协议模块的目录布局”大多数模块采用相同的拆分方式。并非每个模块都有每个文件,模块也会为自身特有的内容增加文件(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 控制。
新增一个协议
Section titled “新增一个协议”-
编写线格式原语,放在
protocols/src/<name>/protocol.rs。用take、take_array和检查过的算术从切片解析;字节不足时通过need_more返回Ok(None);显式检查每个固定字段(版本、保留字节、长度、命令)。地址布局相符时复用AddressCodec::SOCKS或AddressCodec::VMESS,否则声明一个新的AddressCodec值。把结果放进pub const ADDR。 -
编写服务端 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()。 -
编写客户端 codec,放在
codec.rs,基于ProxyCoreEncodeHandshake加ProxyCoreEncode;如果协议承载 UDP,再实现ProxyCoreEncodeDatagram。见客户端运行时。 -
添加用户和配置,放在
users.rs或validator.rs以及config.rs中,对T泛型。用ct_eq比较密钥并扫描整张表,就像trojan::Validator::get那样。绝不记录凭据或密钥。 -
注册模块,在
protocols/src/lib.rs中。如果它带来沉重的依赖树,就像hysteria和tun那样放到一个新的 Cargo feature 之后,并以同样方式控制其 pipeline 测试模块。 -
编写测试。 在
protocols/tests/unit/<name>/中编写通过CoreHarness驱动 core 的单元测试,包括针对格式错误输入、截断输入和未知用户的负向测试。在protocols/tests/pipeline/<name>.rs中添加 pipeline 测试,并在protocols/tests/pipeline.rs中注册。 -
接入 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变体。见从配置到运行的流水线。 -
运行门禁:
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 |