Workspace 与 crate
源码文件:202 个 · 核对版本 Etemenanki 596916d · katana v3.0.1
Etemenanki/Cargo.tomlEtemenanki/Cargo.lockEtemenanki/.gitmodulesEtemenanki/.cargo/config.tomlEtemenanki/.github/workflows/build.ymlEtemenanki/README.mdEtemenanki/concepts/Cargo.tomlEtemenanki/environment/Cargo.tomlEtemenanki/protocols/Cargo.tomlEtemenanki/app/Cargo.tomlEtemenanki/app/src/balancer.rsEtemenanki/app/src/config.rsEtemenanki/app/src/connector.rsEtemenanki/app/src/flow.rsEtemenanki/app/src/inbound/mod.rsEtemenanki/app/src/inbound/tun.rsEtemenanki/app/src/instance.rsEtemenanki/app/src/main.rsEtemenanki/app/src/outbound/freedom.rsEtemenanki/app/src/outbound/mod.rsEtemenanki/app/src/outbound/proxy.rsEtemenanki/app/src/outbound/udp_fanout.rsEtemenanki/app/src/router.rsEtemenanki/app/src/serve.rsEtemenanki/app/src/transport.rsEtemenanki/concepts/src/buffer.rsEtemenanki/concepts/src/client.rsEtemenanki/concepts/src/core.rsEtemenanki/concepts/src/lib.rsEtemenanki/concepts/src/link.rsEtemenanki/concepts/src/net.rsEtemenanki/concepts/src/relay.rsEtemenanki/concepts/src/runtime.rsEtemenanki/concepts/src/sniff.rsEtemenanki/concepts/src/wake.rsEtemenanki/environment/src/dial/mod.rsEtemenanki/environment/src/dial/quic.rsEtemenanki/environment/src/dial/socket.rsEtemenanki/environment/src/dial/tcp.rsEtemenanki/environment/src/dial/udp.rsEtemenanki/environment/src/lib.rsEtemenanki/environment/src/routing.rsEtemenanki/protocols/src/core/harness.rsEtemenanki/protocols/src/core/mod.rsEtemenanki/protocols/src/dns/message.rsEtemenanki/protocols/src/dns/mod.rsEtemenanki/protocols/src/error.rsEtemenanki/protocols/src/flow.rsEtemenanki/protocols/src/helpers/address.rsEtemenanki/protocols/src/helpers/address_family.rsEtemenanki/protocols/src/helpers/crypto.rsEtemenanki/protocols/src/helpers/mod.rsEtemenanki/protocols/src/helpers/parse.rsEtemenanki/protocols/src/http/codec.rsEtemenanki/protocols/src/http/config.rsEtemenanki/protocols/src/http/core.rsEtemenanki/protocols/src/http/mod.rsEtemenanki/protocols/src/http/protocol.rsEtemenanki/protocols/src/hysteria/auth.rsEtemenanki/protocols/src/hysteria/config.rsEtemenanki/protocols/src/hysteria/connection.rsEtemenanki/protocols/src/hysteria/connector.rsEtemenanki/protocols/src/hysteria/mod.rsEtemenanki/protocols/src/hysteria/obfs.rsEtemenanki/protocols/src/hysteria/protocol.rsEtemenanki/protocols/src/hysteria/quic.rsEtemenanki/protocols/src/hysteria/server/authenticator.rsEtemenanki/protocols/src/hysteria/server/config.rsEtemenanki/protocols/src/hysteria/server/datagrams.rsEtemenanki/protocols/src/hysteria/server/endpoint.rsEtemenanki/protocols/src/hysteria/server/inbound.rsEtemenanki/protocols/src/hysteria/server/io.rsEtemenanki/protocols/src/hysteria/server/masquerade.rsEtemenanki/protocols/src/hysteria/server/mod.rsEtemenanki/protocols/src/hysteria/server/shim.rsEtemenanki/protocols/src/hysteria/slot.rsEtemenanki/protocols/src/lib.rsEtemenanki/protocols/src/macros.rsEtemenanki/protocols/src/mux/demux.rsEtemenanki/protocols/src/mux/frame.rsEtemenanki/protocols/src/mux/mod.rsEtemenanki/protocols/src/sniff/collector.rsEtemenanki/protocols/src/sniff/http.rsEtemenanki/protocols/src/sniff/mod.rsEtemenanki/protocols/src/sniff/tls.rsEtemenanki/protocols/src/socks/codec.rsEtemenanki/protocols/src/socks/config.rsEtemenanki/protocols/src/socks/handshake.rsEtemenanki/protocols/src/socks/mod.rsEtemenanki/protocols/src/socks/protocol.rsEtemenanki/protocols/src/socks/server.rsEtemenanki/protocols/src/socks/udp_link.rsEtemenanki/protocols/src/ss_2022/codec.rsEtemenanki/protocols/src/ss_2022/core.rsEtemenanki/protocols/src/ss_2022/crypto.rsEtemenanki/protocols/src/ss_2022/mod.rsEtemenanki/protocols/src/ss_2022/protocol.rsEtemenanki/protocols/src/ss_2022/users.rsEtemenanki/protocols/src/ss_legacy/aead.rsEtemenanki/protocols/src/ss_legacy/codec.rsEtemenanki/protocols/src/ss_legacy/core.rsEtemenanki/protocols/src/ss_legacy/mod.rsEtemenanki/protocols/src/ss_legacy/protocol.rsEtemenanki/protocols/src/ss_legacy/users.rsEtemenanki/protocols/src/transports/accept.rsEtemenanki/protocols/src/transports/connect.rsEtemenanki/protocols/src/transports/grpc/framing.rsEtemenanki/protocols/src/transports/grpc/liveness.rsEtemenanki/protocols/src/transports/grpc/mod.rsEtemenanki/protocols/src/transports/grpc/settings.rsEtemenanki/protocols/src/transports/grpc/stream.rsEtemenanki/protocols/src/transports/keepalive.rsEtemenanki/protocols/src/transports/mod.rsEtemenanki/protocols/src/transports/stream.rsEtemenanki/protocols/src/transports/tls/config.rsEtemenanki/protocols/src/transports/tls/mod.rsEtemenanki/protocols/src/transports/tls/stream.rsEtemenanki/protocols/src/transports/ws/endpoint.rsEtemenanki/protocols/src/transports/ws/mod.rsEtemenanki/protocols/src/transports/ws/stream.rsEtemenanki/protocols/src/trojan/codec.rsEtemenanki/protocols/src/trojan/core.rsEtemenanki/protocols/src/trojan/mod.rsEtemenanki/protocols/src/trojan/protocol.rsEtemenanki/protocols/src/trojan/users.rsEtemenanki/protocols/src/tun/config.rsEtemenanki/protocols/src/tun/device.rsEtemenanki/protocols/src/tun/inbound.rsEtemenanki/protocols/src/tun/mod.rsEtemenanki/protocols/src/tun/tracked.rsEtemenanki/protocols/src/tun/udp.rsEtemenanki/protocols/src/vless/codec.rsEtemenanki/protocols/src/vless/config.rsEtemenanki/protocols/src/vless/core.rsEtemenanki/protocols/src/vless/mod.rsEtemenanki/protocols/src/vless/protocol.rsEtemenanki/protocols/src/vless/validator.rsEtemenanki/protocols/src/vmess/accounts.rsEtemenanki/protocols/src/vmess/aead.rsEtemenanki/protocols/src/vmess/codec.rsEtemenanki/protocols/src/vmess/core.rsEtemenanki/protocols/src/vmess/framing.rsEtemenanki/protocols/src/vmess/keys.rsEtemenanki/protocols/src/vmess/mod.rsEtemenanki/protocols/src/vmess/protocol.rsEtemenanki/protocols/src/vmess/session.rsEtemenanki/protocols/src/wireguard/config.rsEtemenanki/protocols/src/wireguard/connector.rsEtemenanki/protocols/src/wireguard/device.rsEtemenanki/protocols/src/wireguard/mod.rsEtemenanki/protocols/src/wireguard/slot.rsEtemenanki/concepts/tests/runtime.rsEtemenanki/concepts/tests/client.rsEtemenanki/environment/tests/integration.rsEtemenanki/environment/tests/integration/quic.rsEtemenanki/environment/tests/unit/routing.rsEtemenanki/protocols/tests/pipeline.rsEtemenanki/protocols/tests/pipeline/hysteria.rsEtemenanki/protocols/tests/pipeline/socks.rsEtemenanki/protocols/tests/unit/socks/protocol.rsEtemenanki/protocols/tests/unit/socks/server.rsEtemenanki/protocols/tests/unit/vless/protocol.rsEtemenanki/protocols/tests/unit/dns/message.rsEtemenanki/protocols/tests/unit/hysteria/protocol.rsEtemenanki/protocols/tests/unit/mux/frame.rsEtemenanki/protocols/tests/unit/sniff/tls.rsEtemenanki/app/tests/integration.rsEtemenanki/app/tests/support/mod.rsEtemenanki/app/tests/integration/e2e_tun.rsEtemenanki/app/tests/integration/e2e_wg.rsEtemenanki/app/tests/integration/e2e_xray_mux.rskatana/Cargo.tomlkatana/Cargo.lockkatana/.cargo/config.tomlkatana/.gitmoduleskatana/.github/workflows/build.ymlkatana/.github/workflows/release.ymlkatana/src/api/mod.rskatana/src/api/newv2board.rskatana/src/api/sspanel.rskatana/src/config.rskatana/src/connector.rskatana/src/inbound.rskatana/src/main.rskatana/src/manager/mod.rskatana/src/manager/node.rskatana/src/manager/proxy.rskatana/src/manager/transport.rskatana/src/meter.rskatana/src/outbound/freedom.rskatana/src/outbound/mod.rskatana/src/outbound/proxy.rskatana/src/router.rskatana/src/rule.rskatana/src/runtime.rskatana/src/serve.rskatana/src/traffic.rskatana/tests/integration.rskatana/tests/integration/xray_interop.rskatana/tests/integration/hysteria_interop.rskatana/tests/integration/sniff.rskatana/tests/support/mod.rs
代码分布在两个私有仓库中。Etemenanki 是一个包含四个 crate 的 Cargo workspace:其中三个库 crate 共同构成代理内核,另一个是基于它们构建的独立二进制 etemenanki-app。katana 是一个单独的仓库,它的二进制是面板节点 agent,从私有 Cargo registry 获取这三个库 crate。第三个仓库 harranu 存放路由模型,内核现在带有它的一份副本。
动手修改任何代码之前,应先读这份地图。本页涵盖:每个 crate 负责什么,feature 和依赖如何在 crate 之间传递,每个源文件的用途,决定所有解析代码写法的 lint,2.0 版本如何走到单一流水线的设计,以及一次修改必须通过的验证门槛。Concepts、Environment、Protocols 和 App 下的页面再分别深入每个 crate。
文件夹Etemenanki/
- Cargo.toml workspace 清单和共享的依赖版本
- Cargo.lock
- .cargo/config.toml 声明私有 registry
- .github/workflows/build.yml
文件夹concepts/ etemenanki-concepts
- …
文件夹environment/ etemenanki-environment
- …
文件夹protocols/ etemenanki-protocols
- …
文件夹app/ etemenanki-app,独立二进制
- …
文件夹Xray-core/ git submodule,仅用于参考和互操作测试
- …
文件夹hysteria/ git submodule,仅用于参考和互操作测试
- …
文件夹katana/
- Cargo.toml 按版本依赖三个库 crate
- Cargo.lock
- .cargo/config.toml 声明私有 registry
文件夹.github/workflows/ build.yml 和 release.yml
- …
文件夹src/
- …
文件夹tests/
- …
文件夹Xboard/ 面板参考源码树,不参与构建
- …
文件夹V2bX/ 面板参考源码树,不参与构建
- …
文件夹XrayR/ 面板参考源码树,不参与构建
- …
| 仓库 | 构建产物 | 依赖来源 |
|---|---|---|
| Etemenanki | etemenanki-concepts、etemenanki-environment 和 etemenanki-protocols(库,发布到私有 Cargo registry),以及 etemenanki-app(二进制) |
只有 crates.io 和它自己的 path 依赖 |
| katana | katana 二进制 |
来自私有 registry 的三个库 crate:etemenanki-concepts 和 etemenanki-environment 为 2.0.0,etemenanki-protocols 为 2.0.1 |
| harranu | 一个独立的路由模型 crate | 内核和 katana 都已不再依赖它 |
workspace 清单中列出的恰好是 members = ["app", "concepts", "environment", "protocols"]。检出目录中的其他内容,包括各个 submodule,对 Cargo 都不可见。
Xray-core/(上游为 XTLS/Xray-core)和 hysteria/(上游为 HyNetworks/hysteria)是在 .gitmodules 中声明的 git submodule,其中没有任何内容会被 Cargo 编译。它们有两个用途:
- 移植参考。 大多数协议模块都是移植而来,模块文档会写明它参照的 Go 文件:
trojan/protocol.rs移植自proxy/trojan/protocol.go,mux/frame.rs移植自common/mux/{frame.go,reader.go,writer.go},hysteria/obfs.rs移植自hysteria/extras/obfs/salamander.go,而hysteria/PROTOCOL.md是 Hysteria 2 两端实现共同依据的规范。Shadowsocks 2022 移植自sing-shadowsocks/shadowaead_2022,该仓库没有 vendored 进来。 - 互操作测试。
app/tests/support/mod.rs在首次使用时构建上游二进制:在Xray-core/中执行go build -o $CARGO_TARGET_TMPDIR/xray ./main,在hysteria/app中执行go build。构建结果保存在LazyLock静态变量XRAY_BIN和HYSTERIA_BIN中,因此每个测试二进制对每个上游最多只构建一次。如果没有go或构建失败,辅助函数会打印一行SKIP:并返回None,所有需要该二进制的测试都会提前返回并判为通过。katana 的tests/support/mod.rs从同级检出目录../Etemenanki/Xray-core和../Etemenanki/hysteria构建同样的两个二进制,该目录不存在时同样跳过。
katana 的 Xboard/、V2bX/ 和 XrayR/ submodule 在面板兼容性方面起同样的参考作用,不会被构建。
路由模型的来历
Section titled “路由模型的来历”路由模型(first-match 规则表,域名、CIDR 和端口匹配条件,GeoIP 和 GeoSite .dat 加载)迁移过两次:先从 app 和 katana 各自的本地副本迁入共享的 harranu crate,再从 harranu 迁入 etemenanki-environment:
| 步骤 | 仓库 | Commit |
|---|---|---|
app 删除本地副本,改为依赖已发布的 harranu crate |
Etemenanki | 58ad56c Extract route model into shared harranu crate, drop patch hack |
| katana 在同一天做了同样的事 | katana | 90c5e35 Replace vendored route model with the shared harranu crate |
harranu 0.3 原样 vendored 为 etemenanki_environment::routing |
Etemenanki | fc40d1c add etemenanki-environment: host dialers and the vendored route model |
| katana 改用内核中的副本做路由 | katana | e9bc640 route with the kernel’s route model instead of harranu |
最后一次迁移的原因记录在 fc40d1c 中:从私有 registry 依赖 harranu,会让 workspace 在无法访问该 registry 的地方无法构建。对贡献者而言,这意味着路由相关的修改要落在 environment/src/routing.rs。修改 harranu 既影响不到 app,也影响不到 katana。路由模型本身见路由模型。
crate 依赖图
Section titled “crate 依赖图”依赖只有一个方向:从二进制指向 etemenanki-concepts。每个箭头都是清单中声明的一个依赖;标签是依赖方 crate 开启的 feature。
flowchart BT concepts["etemenanki-concepts"] environment["etemenanki-environment"] protocols["etemenanki-protocols"] app["etemenanki-app(二进制)"] katana["katana(二进制,独立仓库)"] environment --> concepts protocols --> concepts protocols --> environment app --> concepts app --> environment app -->|"hysteria, tun"| protocols katana --> concepts katana --> environment katana -->|"hysteria, vendored-openssl"| protocols
在 workspace 内部,每个内部依赖都带有三个键:一个 path(例如 ../concepts),使 workspace 构建使用本地源码;以及 version = "2.0.0" 和指向私有 registry 的 registry,cargo publish 会用它们替换已发布清单中的 path。每个 crate 还把 publish 设为只允许该 registry,因此任何 crate 都不会意外发布到 crates.io。
四个 crate 在 054cf34(“release etemenanki 2.0.0”)中以 2.0.0 一起发布,etemenanki-environment 也在这次发布中首次发布,版本号相同。此后只有 etemenanki-protocols 有过变动,发布了两个 patch 版本,都列在 2.0 重构一节中:2.0.1(2f1f8cb,“release etemenanki-protocols 2.0.1”)包含 WireGuard 和 mux 修复;2.0.2(596916d,“release etemenanki-protocols 2.0.2”)把 SOCKS5 UDP 关联限定在其控制连接的客户端上。2.0.2 没有改变任何公开 API:UDP ASSOCIATE 声明的来源通过 crate 私有的 handshake_with_udp_source 传给 SocksInbound,公开的 handshake 签名保持不变。etemenanki-concepts、etemenanki-environment 和 etemenanki-app 仍为 2.0.0。内部依赖要求仍写作 version = "2.0.0",2.0.2 满足这一要求,workspace 的 Cargo.lock 中记录的 etemenanki-protocols 为 2.0.2。
Feature
Section titled “Feature”没有任何 crate 声明 default feature 集,因此下面每个 feature 都需要显式开启。
| Feature | Crate | 开启的内容 | 由谁开启 |
|---|---|---|---|
hysteria |
etemenanki-protocols |
dep:quinn、dep:h3、dep:h3-quinn、dep:rustls、dep:rustls-native-certs、dep:rustls-pemfile、dep:rustls-pki-types、dep:blake2;编译 protocols::hysteria |
etemenanki-app、katana |
tun |
etemenanki-protocols |
dep:ipstack、dep:tun-rs、dep:rtnetlink(仅限 Linux 的 target 依赖);在 #[cfg(all(feature = "tun", unix))] 下编译 protocols::tun |
etemenanki-app |
vendored-openssl |
etemenanki-protocols |
openssl/vendored:从源码构建 OpenSSL 并静态链接 |
katana |
quic |
etemenanki-environment |
dep:quinn;编译 dial::quic(QuicDialer、SocketWrap) |
没有 crate 开启;只有 --all-features 或显式的 --features quic |
原因记录在各个清单中。hysteria 默认关闭,因为它“pulls in a whole second TLS stack (quinn -> rustls)”,只需要经典协议的 downstream 不应在下一次版本升级时被动继承它。tun 关闭的理由相同:网络接口管理的依赖栈不应进入从不使用它的 crate。katana 开启 hysteria 是因为它要服务 Hysteria 2 节点,开启 vendored-openssl 是因为它的 release 二进制不能动态链接 OpenSSL(见 CI)。
protocols::hysteria 直接在 quinn 之上构建 QUIC endpoint,并不开启 etemenanki-environment/quic,因此在已核对的版本中,没有任何生产构建会编译 QuicDialer。
katana 如何使用内核
Section titled “katana 如何使用内核”katana Cargo.toml 中的依赖 |
版本要求 | Feature | Cargo.lock 中的解析结果 |
|---|---|---|---|
etemenanki-concepts |
version = "2.0.0",私有 registry |
无 | 2.0.0,记录了 registry 来源和校验和 |
etemenanki-environment |
version = "2.0.0",私有 registry |
无 | 2.0.0 |
etemenanki-protocols |
version = "2.0.0",私有 registry |
vendored-openssl、hysteria |
2.0.1 |
裸写的 "2.0.0" 是 caret 版本要求:任何不低于 2.0.0 的 2.x 版本都满足它,因此 etemenanki-protocols 无需修改清单就解析到了 2.0.1。katana v3.0.1 尚未采用 2.0.2,它的 lockfile 仍记录 2.0.1。确切版本由 Cargo.lock 锁定,其中记录了每个 crate 的 registry 来源和校验和;两个 CI workflow 都使用 --locked 构建,因此构建会直接失败,而不会解析到另一个内核版本。要把 katana 升级到新的内核版本,用 cargo update -p <crate> --precise <version> 一次更新一个 package,并确认 lockfile 中没有其他内容发生变化。
两个仓库都在 .cargo/config.toml 中声明该 registry,并使用 cargo:token 凭据提供方。token 从不出现在仓库中,CI 通过环境变量传入。
workspace 设置
Section titled “workspace 设置”| 设置 | 值 | 位置 |
|---|---|---|
| Edition | 2024 |
[workspace.package],被每个 crate 继承 |
| Resolver | 3 |
[workspace] |
| 共享版本 | 一张 [workspace.dependencies] 表;成员写 tokio.workspace = true |
Cargo.toml |
| Release profile | lto = true |
Etemenanki Cargo.toml |
| Release profile(katana) | lto = true、codegen-units = 4 |
katana Cargo.toml |
crate 边界上的关键类型
Section titled “crate 边界上的关键类型”etemenanki-concepts 中的三个 trait 是其他所有 crate 接入的接缝。详见服务端协议核心、服务端运行时和Link、connector 与网络类型。
concepts/src/link.rs → Connector 和 DatagramLink:出站是什么,以及如何打开一个出站。
pub enum Outbound<S, D> { Stream(S), Datagram(D),}
pub trait Connector<Target> { type Stream: AsyncRead + AsyncWrite + Unpin; type Datagram: DatagramLink; type Future: Future<Output = io::Result<Outbound<Self::Stream, Self::Datagram>>>;
fn connect(&mut self, target: Target) -> Self::Future;}
pub trait DatagramLink: Unpin { type Addr;
fn poll_send_to( &mut self, cx: &mut Context<'_>, buf: &[u8], to: &Self::Addr, ) -> Poll<io::Result<usize>>;
fn poll_recv_from( &mut self, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll<io::Result<Self::Addr>>;}concepts/src/core.rs → ProxyCoreDecode:协议的服务端,实现为一个 sans-I/O 状态机。
pub trait ProxyCoreDecode { type Key: Copy + Ord + Send + Sync + 'static; type Target; type Error; type TransportAddr: Clone + Send + Sync + 'static;
const STAGING_RESERVE: usize; const MAX_DATAGRAM: usize = 4096;
fn handle( &mut self, event: Event<'_, Self>, effects: &mut Effects<'_, Self>, ) -> Result<usize, Self::Error>;
fn held(&self) -> &[u8] { .. }}concepts/src/runtime.rs → ProxyServerRuntime:每条连接的驱动器,把协议核心、它的传输层和一个 connector 连在一起。
pub struct ProxyServerRuntime<const BUF_SIZE: usize, Core, Trans, Conn, Mode = ProxyRunsQuiet>where Core: ProxyCoreDecode, Conn: Connector<Core::Target>,{ .. }
impl<const BUF_SIZE: usize, Core, T, Conn> ProxyServerRuntime<BUF_SIZE, Core, StreamTransport<T>, Conn, ProxyRunsQuiet>where Core: ProxyCoreDecode, T: AsyncRead + AsyncWrite + Unpin, Conn: Connector<Core::Target>,{ pub fn new(transport: T, core: Core, connector: Conn) -> Self { .. }}
impl<const BUF_SIZE: usize, Core, D, Conn> ProxyServerRuntime<BUF_SIZE, Core, DatagramTransport<D>, Conn, ProxyRunsQuiet>where Core: ProxyCoreDecode, D: DatagramLink<Addr = Core::TransportAddr>, Conn: Connector<Core::Target>,{ pub fn over_datagrams(link: D, core: Core, connector: Conn) -> Self { .. }}new 包装一条字节流,over_datagrams 包装一个 DatagramLink;两者都断言 BUF_SIZE > Core::STAGING_RESERVE,因为缓冲区再小就永远腾不出读取的空间。showing_progress 把安静的 Future 变成下文 tokio-stream 一行所述、产出 Traffic 报告的 Stream。
protocols/src/flow.rs → Flow:etemenanki-protocols 中每个服务端协议核心的 Target,因而也是 app 和 katana 的 connector 收到的东西。
pub struct Flow<T> { pub destination: Destination, pub user: NetworkUser<T>, pub sniffed: Option<SniffedBehavior>, pub source: Option<IpAddr>,}在已核对的版本中,各 trait 的实现者如下:
| Trait | 实现 |
|---|---|
ProxyCoreDecode |
TrojanCore、VlessCore、VMessCore、ShadowsocksCore、Ss2022Core、HttpCore、PassthroughCore、Hy2StreamCore、Hy2UdpCore、TunUdpCore。SOCKS 是例外:socks/server.rs → SocksInbound 自己驱动控制连接。 |
ProxyCoreEncode(客户端 codec) |
TrojanStream、VlessStream、VMessStream、SsStream、Ss2022Stream、SocksConnect、HttpConnect 和 NoCodec |
ProxyCoreEncodeDatagram |
TrojanDatagram、VlessDatagram、VMessDatagram 和 NoCodec |
Connector<Target> |
concepts:SocketConnector、ProxyClientConnector、所有 FnMut(Target) -> Fut 闭包;environment:Dialer(针对 DialTarget 和 SocketTarget);protocols:TransportConnector、WgConnector、Hy2Connector;app:AppConnector、FreedomConnector;katana:KatanaConnector |
DatagramLink |
concepts:UdpSocket、UdpOutbound、NoDatagram、ProxyClientRuntime;environment:DualStackUdp;protocols:SocksUdpLink、WgDatagramLink、Hy2DatagramLink、QuicDatagrams、TunUdpLink;app:FanOutLink、ResolvingUdp、OutboundDatagram、BlackholeLink;katana:FanOut、ResolvingUdp、OutboundDatagram |
| 依赖 | Crate | 用途 |
|---|---|---|
tokio(full) |
全部 | 运行时、socket 和定时器。每个 crate 的 dev-dependencies 都加上了 test-util,用于 start_paused 测试,因为空闲和 keepalive 的截止时间长达数分钟。 |
tokio-stream |
concepts、protocols、app | Stream trait:在 ProxyShowsProgress 模式下,ProxyServerRuntime 不是 Future,而是一个 Stream,其元素是 Result<Traffic, RuntimeError<_>> 形式的进度报告。 |
tokio-util |
protocols、app | CancellationToken:app 把一个 generation 的所有任务都挂在同一个 token 下(serve.rs → spawn_scoped),Hysteria 2 和 TUN 入站也用 token 停止各自的任务。AbortOnDropHandle 把一个后台任务绑定到拥有它的值上:拨号得到的 gRPC stream 的 HTTP/2 驱动,以及 Hysteria 2 客户端连接的 HTTP/3 驱动和数据报泵。 |
futures |
protocols | tokio-tungstenite socket 之上的 Sink 和 Stream trait(ws/stream.rs),以及一个 Shared future,让并发调用方等待同一次 Hysteria 2 连接构建(hysteria/slot.rs)。 |
pin-project |
concepts | relay.rs 中手写 future 的 pin 投影。 |
compact_str、smallvec |
concepts 及以上 | Remote::Domain 持有一个 CompactString;EffectList 是带 INLINE_EFFECTS = 4 个内联槽位的 SmallVec,因此典型的事件不做任何分配。 |
parking_lot |
concepts、protocols、app | 持有时间短、从不跨 await 的锁:wake.rs → ReadyQueue、用户表、可重建的连接槽。 |
arc-swap |
protocols、app | 重载时在运行中的监听器下替换的用户表,例如 vmess/accounts.rs 和 Hysteria 2 的 authenticator。 |
rand |
protocols | salt、请求填充、VMess 会话密钥和 DNS 查询 ID。 |
openssl、tokio-openssl |
protocols | 所有基于 TCP 的 TLS 会话:TLS 传输层(transports/tls/config.rs 用 SslAcceptor::mozilla_intermediate_v5 构建服务端,并在两端都把最低版本设为 TLS 1.2),以及 dns/mod.rs 中的 DNS-over-TLS 和 DNS-over-HTTPS 后端。 |
quinn、h3、h3-quinn、rustls、rustls-native-certs、rustls-pemfile、rustls-pki-types |
protocols,feature hysteria;quinn 单独也出现在 environment 中,feature quic |
Hysteria 2 的 QUIC 和 HTTP/3,以及 QUIC 拨号器。引入 rustls 只是因为它是 quinn 唯一的加密后端。 |
blake2 |
protocols,feature hysteria |
Salamander 混淆器使用的哈希。 |
boringtun、smoltcp |
protocols(始终引入) | WireGuard:boringtun 的 Tunn 运行 WireGuard 协议,smoltcp 是隧道内的用户态 TCP/IP 协议栈。不受 feature 控制。 |
h2、http |
protocols | h2 承载基于 HTTP/2 的 gRPC 传输(transports/grpc/,以及 transports/accept.rs 中把一条 HTTP/2 连接作为多条 stream 来服务)。http 类型还用于 WebSocket 升级头,以及 Hysteria 2 的 HTTP/3 认证和伪装。 |
tokio-tungstenite |
protocols | WebSocket 传输层。 |
httparse |
protocols | http/protocol.rs 中的 HTTP/1.x 请求头和响应头。 |
ipstack、tun-rs、rtnetlink |
protocols,feature tun |
用户态 IP 协议栈、设备创建,以及 Linux 上的路由安装。 |
aes、aes-gcm、chacha20poly1305、hkdf、sha1、sha2、md-5、crc32fast、blake3、subtle |
protocols | VMess、Shadowsocks 和 Trojan 的加密算法、KDF 和哈希:md-5 用于 Shadowsocks 的 EVP_BytesToKey、VMess 命令密钥和 VMess ChaCha20-Poly1305 正文密钥;HKDF-SHA1 用于 Shadowsocks AEAD 子密钥;SHA-256 用于 VMess KDF;SHA-224 用于 Trojan 密码哈希;CRC32 用于 VMess auth id 中的校验和;BLAKE3 用于 Shadowsocks 2022 的会话子密钥和身份子密钥。 |
socket2 |
environment、protocols | 必须在创建之后、bind 或 connect 之前设置的 socket 选项(SocketOptions),以及 TCP keepalive(transports/keepalive.rs)。 |
cidr、regex、prost |
environment | 路由匹配条件;prost 解码 geoip.dat 和 geosite.dat 中的 protobuf。app 和 katana 也直接使用 cidr。 |
moka(future) |
protocols | dns/mod.rs 中的 DNS 应答缓存,一个上限为 CACHE_CAPACITY = 8192 个域名的 moka::future::Cache。 |
notify |
app、katana | 热重载背后的配置文件监视器。 |
serde、toml、clap、tracing-subscriber |
app、katana | 配置、命令行和日志输出。 |
reqwest(native-tls-vendored)、serde_json |
katana | 面板 HTTP 客户端及其 JSON 请求体和响应体。在 Linux 上 native-tls 就是 OpenSSL,vendored 变体会静态构建它,与内核的 vendored-openssl 一样。 |
thiserror、anyhow |
protocols | ProtocolError,以及它不透明的 Other 变体。 |
uuid |
concepts、protocols、app | VMess 和 VLESS 用户的 UserAuthorization::Uuid。 |
仅用于开发的依赖:environment 中的 rcgen、rustls 和 rustls-pki-types(为 QUIC 拨号器测试生成证书),protocols 中的 etherparse(为假 TUN 设备构造 IP 数据包),以及 app 中的 openssl、boringtun 和 smoltcp(测试证书和进程内的 WireGuard 对端)。除了带 test-util 的 tokio,katana 唯一的 dev-dependency 是带 vendored 的 openssl,用于为它的 Xray TLS 测试生成自签名证书;vendored 与内核的 vendored-openssl 保持一致,因此两者解析到同一份 OpenSSL 构建。
两套 TLS 栈,一条规则
Section titled “两套 TLS 栈,一条规则”所有基于 TCP 的 TLS 会话都由 OpenSSL 终结。rustls 只随 quinn 进入构建:要么在 etemenanki-protocols 的 hysteria feature 下,要么在 etemenanki-environment 的 quic feature 下,而后者没有 crate 开启。之所以引入它,是因为 QUIC 把 TLS 嵌入了自己的握手状态机,而 quinn 不提供其他后端。代码构建的两份 rustls 配置都在 protocols::hysteria 中,并且都显式指定了 provider:
let provider = Arc::new(rustls::crypto::ring::default_provider());let builder = rustls::ClientConfig::builder_with_provider(provider)即客户端的 hysteria/connection.rs → tls_config 和服务端的 hysteria/server/endpoint.rs → tls_config。当 downstream 在构建中统一进第二个 crypto provider feature 时,普通的 ClientConfig::builder() 和 ServerConfig::builder() 会 panic,因此绝不能使用。服务端配置还固定使用 TLS 1.3:QuicServerConfig 在收到第一个数据包时会对 rustls::quic::ServerConnection::new 调用 unwrap,所以不含 TLS 1.3 的配置会在 accept 循环里 panic,而不是在构建配置时报错。
行数取自已核对的版本,包含文档注释。单元测试放在 src/ 之外(见测试),所以行数只包括代码和文档,唯一的例外是 etemenanki-concepts 中三个小的内联 mod tests 块。
etemenanki-concepts
Section titled “etemenanki-concepts”3,889 行。sans-I/O 模型:服务端协议核心、客户端 codec、两种运行时、link 和 connector。它不含任何协议代码,也不依赖任何其他 workspace crate。
| 文件 | 行数 | 职责 |
|---|---|---|
lib.rs |
44 | crate 文档(包括单任务示意图)和模块列表 |
core.rs |
732 | 协议 trait。服务端:ProxyCoreDecode、Event、Effect、Effects、EffectList。客户端:ProxyCoreEncodeHandshake、ProxyCoreEncode、ProxyCoreEncodeDatagram、Handshake、Reply、Opened、NoCodec |
runtime.rs |
1,394 | ProxyServerRuntime,Transport trait 及 StreamTransport 和 DatagramTransport,Traffic、RuntimeError,ProxyRunsQuiet 和 ProxyShowsProgress 两种模式,WORK_BUDGET |
client.rs |
654 | ProxyClientRuntime(拨号得到的上游之上的一个 codec,以字节流或 DatagramLink 的形式暴露)、ProxyClientConnector、ProxyClientConnecting |
link.rs |
223 | DatagramLink、Outbound、Connector 及其闭包实现、UdpOutbound、SocketConnector、SocketTarget、NoStream、NoDatagram |
buffer.rs |
269 | ReadBuffer 和 WriteBuffer:从不增长的定长装箱数组;以及 Staging:WriteBuffer 空闲尾部的只追加视图,供协议核心写入 |
wake.rs |
180 | ReadyQueue 和 KeyWaker:按 key 唤醒,使运行时只轮询已就绪的出站 |
relay.rs |
277 | UnidirectionalConnection、BidirectionalConnection、Relayed:直通路径上复制字节的 future |
net.rs |
94 | UserAuthorization、NetworkUser、DialNetwork、Remote、Destination |
sniff.rs |
22 | SniffedProtocol、SniffedBehavior、Sniffer trait |
etemenanki-environment
Section titled “etemenanki-environment”1,363 行。互不依赖的两半:本机如何打开 socket,以及一条流被路由到哪个出站。
| 文件 | 行数 | 职责 |
|---|---|---|
lib.rs |
40 | crate 文档和 lint 策略 |
dial/mod.rs |
100 | DialTarget、Dialer,以及它针对 DialTarget 和 SocketTarget 的 Connector 实现 |
dial/socket.rs |
247 | SocketOptions(源地址、Interface、packet mark、SocketHook)、AddressFamily,以及在编译期确定的各操作系统支持情况 |
dial/tcp.rs |
109 | TcpDialer,在多个地址间尝试的 connect_any,每次尝试的 DEFAULT_CONNECT_TIMEOUT = 10 秒 |
dial/udp.rs |
187 | UdpDialer、bind_dual、DualStackUdp |
dial/quic.rs |
90 | feature quic:QuicDialer、SocketWrap |
routing.rs |
590 | vendored 进来的 harranu 路由模型:RouteTarget、RouteMatch、RouteTable、DomainRegex、GeoData、.dat 的 protobuf 消息 |
拨号器见拨号器与 socket 策略,路由见路由模型。
etemenanki-protocols
Section titled “etemenanki-protocols”23,105 行。每个协议都是在上述 trait 之上的一个服务端协议核心加一个客户端 codec;传输层把 socket 变成字节流;共享部分与它们并列。
| 区域 | 行数 | 页面 |
|---|---|---|
crate 根、core/、helpers/ |
1,487 | 协议 crate 基础 |
sniff/ |
360 | 嗅探 |
dns/ |
748 | DNS |
transports/ |
2,356 | TCP 与 TLS、WebSocket 与 gRPC |
mux/ |
929 | mux.cool 与 XUDP |
socks/、http/ |
1,422 和 760 | SOCKS、HTTP |
trojan/、vless/ |
928 和 1,000 | Trojan、VLESS |
vmess/ |
2,876 | VMess 加密、VMess 线格式 |
ss_legacy/、ss_2022/ |
1,334 和 1,668 | Shadowsocks、Shadowsocks 2022 |
hysteria/(feature hysteria) |
4,739 | Hysteria 2 客户端、Hysteria 2 服务端 |
wireguard/ |
1,555 | WireGuard |
tun/(feature tun,Unix) |
943 | TUN |
| 文件 | 行数 | 职责 |
|---|---|---|
lib.rs |
38 | lint 策略和模块列表,带有 hysteria 和 tun 两个开关 |
flow.rs |
70 | Flow,每个协议核心的目标类型 |
error.rs |
72 | ProtocolError 及其到 io::ErrorKind 的映射 |
macros.rs |
23 | byte_newtype!:用于密钥材料的定长字节 newtype |
core/mod.rs |
464 | FlowKey、SubKey、Phase、Timing、HANDSHAKE_TIMEOUT、RELAY_IDLE_TIMEOUT、SniffPrefix、Passthrough、PassthroughCore |
core/harness.rs |
125 | CoreHarness:手动驱动的运行时替身,用于在没有 socket 的情况下测试协议核心 |
helpers/address.rs |
354 | AddressCodec(SOCKS5、Trojan、Shadowsocks、VLESS 和 VMess 使用的带类型字节的地址格式)、parse_authority、format_authority |
helpers/address_family.rs |
230 | AddressFamilyStrategy、FamilySupport、resolve_candidates、destination_to_socketaddrs |
helpers/crypto.rs |
54 | evp_bytes_to_key、hkdf_sha1_ss_subkey、increment_le、ct_eq |
helpers/parse.rs |
53 | take、take_array、need_more:不会 panic 的切片访问(见 Lint) |
helpers/mod.rs |
4 | 模块列表 |
sniff/mod.rs |
84 | SNIFF_TIMEOUT、SNIFF_LIMIT、sniff、worth_sniffing、plausible_domain |
sniff/collector.rs |
77 | Collector 和 Verdict:累积一条流开头字节的累加器,与时钟无关 |
sniff/tls.rs |
101 | TlsSniffer:ClientHello 中的 SNI |
sniff/http.rs |
98 | HttpSniffer:HTTP/1.x 的 Host |
dns/mod.rs |
545 | Resolver、Backend、ResolverSpec,缓存及其 TTL 上下限 |
dns/message.rs |
203 | 最小化的 A/AAAA 线格式编解码:encode_query、decode_answer |
| 文件 | 行数 | 职责 |
|---|---|---|
transports/mod.rs |
14 | 模块列表 |
transports/accept.rs |
175 | InboundTransport、Accepted、TRANSPORT_HANDSHAKE_TIMEOUT,把一条 HTTP/2 连接作为多条 stream 来服务 |
transports/connect.rs |
195 | TransportKind、TransportConnector:解析、连接并包装上游 |
transports/stream.rs |
71 | TransportStream:所有传输层产出的唯一字节流类型 |
transports/keepalive.rs |
33 | set_keepalive,作用于每个 accept 或拨号得到的 socket |
transports/tls/mod.rs |
8 | 模块列表 |
transports/tls/config.rs |
201 | 基于 OpenSSL 的 ServerConfig、ClientConfig、Alpn、VerifyMode |
transports/tls/stream.rs |
91 | MaybeTlsStream、tcp_from_std、accept_optional_tcp、wrap_optional_tcp |
transports/ws/mod.rs |
7 | 模块列表 |
transports/ws/endpoint.rs |
254 | WsRoute、WsTarget,path 和 early data 的处理,MAX_EARLY_DATA、MAX_WS_MESSAGE_LEN |
transports/ws/stream.rs |
378 | WsStream:把 WebSocket 消息当作字节流,WS_IDLE_TIMEOUT、WS_KEEPALIVE_INTERVAL |
transports/grpc/mod.rs |
10 | 模块列表 |
transports/grpc/framing.rs |
320 | encode_hunk、encode_multi_hunk、HunkDecoder |
transports/grpc/settings.rs |
149 | HTTP/2 设置、MAX_GRPC_MESSAGE_LEN、H2_IDLE_TIMEOUT、GrpcPaths、GrpcMode |
transports/grpc/liveness.rs |
145 | Liveness:被服务连接的空闲截止时间和 PING 监督 |
transports/grpc/stream.rs |
305 | GrpcStream:把一条 HTTP/2 stream 当作字节流 |
mux.cool
Section titled “mux.cool”| 文件 | 行数 | 职责 |
|---|---|---|
mux/mod.rs |
52 | MUX_ADDRESS、MUX_PORT、mux_destination、is_mux_destination |
mux/frame.rs |
407 | mux.cool 帧编解码、FrameMeta、SessionStatus、MAX_META_LEN、MAX_DATA_LEN |
mux/demux.rs |
470 | Demux:Trojan、VLESS 和 VMess 协议核心共用的服务端解复用器;普通承载者用 feed,一次 VMess 读取得到的所有分块用 feed_chunks 一次调用送入 |
SOCKS 与 HTTP
Section titled “SOCKS 与 HTTP”| 文件 | 行数 | 职责 |
|---|---|---|
socks/mod.rs |
14 | 模块列表 |
socks/protocol.rs |
249 | SOCKS4、4a 和 5 的线格式常量与基础操作;endpoint,即两端比较中继数据报发送方时所用的规范形式 (IpAddr, u16) |
socks/handshake.rs |
273 | 直到回复为止的服务端握手:handshake、handshake_with_udp_source(crate 私有;还会返回 UDP ASSOCIATE 指明的来源)、Request、Version、回复写入函数 |
socks/server.rs |
445 | SocksInbound:拥有控制连接的专用驱动;ExpectedSender,即一个 UDP 关联唯一接收的那个客户端 |
socks/codec.rs |
154 | SocksConnect:SOCKS5 CONNECT 客户端 codec |
socks/udp_link.rs |
207 | SocksUdpLink:把客户端侧的 UDP ASSOCIATE 作为一个 DatagramLink |
socks/config.rs |
80 | SocksServerConfig、SocksAuth |
http/mod.rs |
11 | 模块列表 |
http/protocol.rs |
283 | 请求头和响应头、固定响应、转发请求的改写 |
http/core.rs |
322 | HttpCore:CONNECT 隧道和 absolute-form 转发 |
http/codec.rs |
96 | HttpConnect:CONNECT 客户端 codec |
http/config.rs |
48 | HttpServerConfig |
Trojan 与 VLESS
Section titled “Trojan 与 VLESS”| 文件 | 行数 | 职责 |
|---|---|---|
trojan/mod.rs |
11 | 模块列表 |
trojan/protocol.rs |
332 | 请求头和 UDP 数据包的分帧、password_hash |
trojan/users.rs |
89 | TrojanServerConfig、Validator |
trojan/core.rs |
353 | TrojanCore |
trojan/codec.rs |
143 | TrojanStream、TrojanDatagram |
vless/mod.rs |
15 | 模块列表 |
vless/protocol.rs |
309 | 请求头和响应头、带长度前缀的 UDP 数据包 |
vless/validator.rs |
77 | Validator,即用户表 |
vless/config.rs |
49 | VlessServerConfig |
vless/core.rs |
365 | VlessCore |
vless/codec.rs |
185 | VlessStream、VlessDatagram |
| 文件 | 行数 | 职责 |
|---|---|---|
vmess/mod.rs |
18 | 模块列表 |
vmess/aead.rs |
616 | HMAC-SHA256 KDF、AES-128 auth id 编解码、密封的头部信封 |
vmess/keys.rs |
211 | 带类型的 16 字节密钥材料,使密钥不会被误传到需要 IV 的位置 |
vmess/accounts.rs |
358 | Account、AccountValidator:把 auth id 匹配到用户 |
vmess/protocol.rs |
419 | 请求头和响应头编解码、Security、RequestOptions |
vmess/framing.rs |
388 | ChunkStream、ChunkDecoder:正文分块的分帧 |
vmess/session.rs |
108 | OutboundSession:每条连接的密钥和 IV 派生 |
vmess/core.rs |
563 | VMessCore |
vmess/codec.rs |
195 | VMessStream、VMessDatagram |
Shadowsocks
Section titled “Shadowsocks”| 文件 | 行数 | 职责 |
|---|---|---|
ss_legacy/mod.rs |
16 | 模块列表;支持的加密方法 |
ss_legacy/aead.rs |
656 | Method、Session、ChunkEncoder、ChunkDecoder、EncryptWriter、DecryptReader |
ss_legacy/users.rs |
97 | ShadowsocksServerConfig、Resolved 密钥表 |
ss_legacy/protocol.rs |
9 | 共享的 ADDR 编解码 |
ss_legacy/core.rs |
433 | ShadowsocksCore |
ss_legacy/codec.rs |
123 | SsStream |
ss_2022/mod.rs |
13 | 模块列表 |
ss_2022/crypto.rs |
523 | 2022-blake3-* 加密方法、session_key、identity_subkey、StreamAead、ChunkWriter、ChunkReader |
ss_2022/protocol.rs |
381 | 请求头和响应头的分帧、扩展身份头 |
ss_2022/users.rs |
164 | Ss2022ServerConfig、Validator、normalise_psk、decode_psk |
ss_2022/core.rs |
420 | Ss2022Core |
ss_2022/codec.rs |
167 | Ss2022Stream |
Hysteria 2
Section titled “Hysteria 2”| 文件 | 行数 | 职责 |
|---|---|---|
hysteria/mod.rs |
29 | 模块列表和共享连接模型 |
hysteria/protocol.rs |
821 | QUIC varint、TCPRequest 和 TCPResponse、填充、UDP 消息及分片重组 |
hysteria/auth.rs |
177 | HTTP/3 认证交互 |
hysteria/obfs.rs |
341 | Salamander 数据包混淆 |
hysteria/quic.rs |
66 | 两端共用的数据报发送:先整条发送,只有对端拒收整条消息时才分片 |
hysteria/connection.rs |
594 | Hy2Conn:一条已认证的客户端连接,以及 rustls 客户端配置 |
hysteria/slot.rs |
235 | ConnSlot:延迟构建、可重建的共享连接 |
hysteria/connector.rs |
216 | Hy2Connector、Hy2Stream、Hy2DatagramLink |
hysteria/config.rs |
79 | Hy2Config、Obfs |
hysteria/server/mod.rs |
12 | 模块列表 |
hysteria/server/inbound.rs |
682 | Hy2Inbound、Hy2StreamCore |
hysteria/server/shim.rs |
525 | 在 HTTP/3 和代理 stream 之间拆分同一条 QUIC 连接 |
hysteria/server/datagrams.rs |
357 | QuicDatagrams、Hy2UdpCore |
hysteria/server/authenticator.rs |
216 | Authenticator:判断一个凭据属于哪个用户 |
hysteria/server/endpoint.rs |
131 | 服务端 QUIC endpoint 及其 rustls 配置 |
hysteria/server/io.rs |
114 | QuicIo:把 h3 的 stream 类型包装为 AsyncRead 和 AsyncWrite |
hysteria/server/masquerade.rs |
82 | Masquerade:对非客户端访问者给出的应答 |
hysteria/server/config.rs |
62 | ServerConfig、ListenerConfig |
WireGuard 与 TUN
Section titled “WireGuard 与 TUN”| 文件 | 行数 | 职责 |
|---|---|---|
wireguard/mod.rs |
41 | 模块文档:为什么 WireGuard 只作为出站 |
wireguard/config.rs |
97 | WgConfig、parse_key |
wireguard/device.rs |
998 | WgDevice:唯一的驱动任务,拥有 boringtun 的 Tunn、smoltcp 协议栈和对端 socket;每条连接最多持有一个上行项,CHANNEL_CAP = 256 |
wireguard/connector.rs |
268 | WgConnector、WgStream、WgDatagramLink |
wireguard/slot.rs |
151 | DeviceSlot:延迟构建、可重建的隧道 |
tun/mod.rs |
42 | 模块文档:哪些流量会被丢弃,以及如何让出站路径绕开该设备 |
tun/config.rs |
23 | TunConfig、DEFAULT_MTU、DEFAULT_UDP_IDLE_TIMEOUT、DEFAULT_MAX_FLOWS |
tun/device.rs |
196 | DeviceSpec、open、TunDevice |
tun/inbound.rs |
303 | 基于 ipstack 的 TunInbound |
tun/udp.rs |
286 | TunUdpLink、TunUdpCore |
tun/tracked.rs |
93 | TrackedTcp:被计数的 stream,使关闭流程可以等待它们结束 |
etemenanki-app
Section titled “etemenanki-app”4,302 行。一个二进制 crate:没有公共 API,所有模块都是 main.rs 的私有模块。
| 文件 | 行数 | 职责 |
|---|---|---|
main.rs |
149 | CLI(-c/--config、--test)、tracing 设置、带 200 ms 防抖的 notify 监视器、SIGINT/SIGTERM |
config.rs |
666 | TOML schema(处处使用 deny_unknown_fields)、load、parse_bytes、重载时的差异比较 |
instance.rs |
391 | Instance、build、Built:generation 与热重载 |
serve.rs |
413 | spawn_scoped、StreamListener、accept 循环、run_hysteria_inbound |
connector.rs |
39 | AppConnector:为一条流选路,并在选中的出站上打开它 |
flow.rs |
22 | Flow、FlowContext |
router.rs |
158 | build_router、route_target:把 [[route.rule]] 编译成 RouteTable |
transport.rs |
156 | tls_layer、resolve_stream、reject_stream_settings:两个构建器共用的 stream 设置校验 |
balancer.rs |
188 | Balancer、Member、Strategy:带健康探测的出站组 |
inbound/mod.rs |
608 | BindSpec、StreamInbound、InboundKind、build_inbound |
inbound/tun.rs |
105 | build_tun_inbound、run_tun_inbound |
outbound/mod.rs |
844 | Outbound、build_outbound,各协议客户端的装配 |
outbound/proxy.rs |
215 | ProxyClient、OutboundStream、OutboundDatagram、BlackholeLink |
outbound/freedom.rs |
163 | FreedomConnector、ResolvingUdp |
outbound/udp_fanout.rs |
185 | FanOutLink:逐包的 UDP 路由,MAX_SUBS |
app 的内部实现见从配置到运行的流水线、入站服务、出站、扇出与负载均衡和Generation 与热重载。
katana
Section titled “katana”6,460 行,一个二进制 crate。详见 katana 内部结构。
| 文件 | 行数 | 职责 |
|---|---|---|
main.rs |
54 | CLI(-c/--config、--test) |
config.rs |
324 | TOML schema |
runtime.rs |
439 | 进程根:出站池、每个 [[node]] 一个 NodeManager、配置监视;重载时先构建所有新增节点,再改动正在运行的节点 |
api/mod.rs |
369 | 覆盖两种面板类型的 PanelClient、panel_node_type |
api/sspanel.rs |
576 | sspanel(mod_mu)客户端 |
api/newv2board.rs |
452 | newV2board(UniProxy)客户端 |
manager/mod.rs |
97 | NodeManager → TransportManager → ProxyManager 层级 |
manager/node.rs |
675 | 每个节点的同步与变化分类;启动引导按 1 秒到 60 秒的退避重试;[node.api] 修改时重建面板客户端 |
manager/transport.rs |
177 | 一个已绑定的监听器及服务它的组件 |
manager/proxy.rs |
239 | 一个节点的用户表与准入 |
inbound.rs |
548 | 按面板节点类型构造入站 |
serve.rs |
447 | accept 循环,每条连接一个运行时 |
connector.rs |
361 | KatanaConnector:对每条流做准入、路由和审计 |
router.rs |
120 | 基于 etemenanki_environment::routing 编译 [[route.rule]] |
rule.rs |
76 | 目的地审计规则 |
traffic.rs |
472 | 按用户的流量计数器和限速 |
meter.rs |
141 | 在流的出站一侧计量 |
outbound/mod.rs |
533 | 出站池 |
outbound/proxy.rs |
179 | 通往上游出站的代理客户端 |
outbound/freedom.rs |
181 | 直连出站 |
crate 级 lint
Section titled “crate 级 lint”environment/src/lib.rs 和 protocols/src/lib.rs 开头是同一个属性:
#![deny( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::arithmetic_side_effects)]// 测试使用已知合法的输入,可以随意使用会 panic 的写法。#![cfg_attr( test, allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::arithmetic_side_effects ))]这四个 lint 合起来,禁止了来自网络的一个字节让进程 panic 的常见途径:.unwrap() 和 .expect(),buf[i] 和 buf[a..b],以及任何可能溢出或 panic 的整数运算,包括 usize 上的 +。它们并不覆盖所有 panic(显式的 assert! 或 unreachable! 仍能编译通过),所以只是底线,而不是证明。etemenanki-concepts、etemenanki-app 和 katana 没有这个属性。
仅有的局部例外是 etemenanki-protocols 中的八个函数,它们只重新允许 clippy::arithmetic_side_effects 这一项,每个都附带一个说明边界的 reason:
| 函数 | 文件 | 给出的理由 |
|---|---|---|
put_varint、read_varint |
transports/grpc/framing.rs |
对 u64 按常量 7 移位;移位量保持在 64 以下 |
read_varint、read_varint_slice |
hysteria/protocol.rs |
QUIC varint 前缀把累加结果限制在 62 位以内 |
decode_varint |
hysteria/server/shim.rs |
同样的 62 位上限 |
deadline |
transports/grpc/liveness.rs |
Instant 只有超过单调时钟的终点才会溢出 |
after |
transports/ws/stream.rs、socks/server.rs |
同样的 Instant 论证 |
新增例外时沿用同样的形式:一个小函数、一个 lint,以及一个审阅者可以核实的 reason。
cfg_attr(test, …) 这一半很重要,原因在于单元测试的编译方式:它们被挂载到所测试的模块中(见测试),因此在 cfg(test) 下属于 crate 的一部分,否则也会继承这些禁令。
对解析器意味着什么
Section titled “对解析器意味着什么”etemenanki-protocols 中的解析器接收一个可能不完整、而且肯定不可信的字节切片,结果只有三种:一个值加上它用掉的字节数,“字节还不够”,或者一个错误。helpers/parse.rs 提供了基本构件:
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>>;take就是data.get(index),只是把读取不足映射为ProtocolError::Truncated(what)。take_array用checked_add计算结束偏移(溢出变为ProtocolError::Overflow("field offset")),并复制出一个定长数组,这正是u16::from_be_bytes一类函数需要的。need_more把截断(到达io::Error时表现为UnexpectedEof)变为Ok(None),其他错误仍保持为错误。拿到一个不断增长的缓冲区的解析器,就是靠它表达“等字节更多了再来问我”。
vless/protocol.rs → parse_request_header 展示了由此形成的风格。单个字节通过 first() 和 get() 获取,固定字段一旦可用就立即校验,偏移用 saturating_add 或 checked_add 计算:
pub fn parse_request_header(buf: &[u8]) -> io::Result<Option<(RequestHeader, usize)>> { let Some(&version) = buf.first() else { return Ok(None); }; if version != VERSION { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("invalid vless request version: {version}"), )); } let Some(uuid) = need_more(take_array::<16>(buf, 1).map_err(io::Error::from))? else { return Ok(None); }; // ……附加信息长度、命令,然后通过 `take(buf, 19.., "vless address")` 读取地址}错误来自 error.rs → ProtocolError,它对出错的原因做了分类。它的 From<ProtocolError> for io::Error 实现确定了协议层以上的调用方看到的 io::ErrorKind:
| 变体 | 含义 | io::ErrorKind |
|---|---|---|
Truncated(&'static str) |
输入比协议要求的短 | UnexpectedEof |
Overflow(&'static str) |
对不可信长度或偏移的算术运算溢出 | InvalidData |
Malformed(&'static str) |
某个字段的值是协议不允许的 | InvalidData |
Unsupported(&'static str) |
本实现不提供的功能 | InvalidData |
Unauthenticated(&'static str) |
无法认证对端 | PermissionDenied |
Crypto(&'static str) |
某个 AEAD 或密钥派生步骤失败 | Other |
Io(io::Error) |
底层 I/O 失败 | 原样透传 |
Other(anyhow::Error) |
任何无法分类的错误 | Other |
UnexpectedEof 这一映射是承重的:need_more 依靠它区分“等待”和“拒绝”。
解析器测试遵循一个共同模式:编码一条合法消息,在许多位置截断它,并断言不会 panic,且每个前缀要么是“需要更多字节”,要么是错误。例如:
| 测试 | 文件 | 固定的行为 |
|---|---|---|
request_header_is_parsed_from_a_slice_once_whole |
protocols/tests/unit/vless/protocol.rs |
VLESS 请求头在字段边界处、地址内部以及差一个字节时被截断,结果都是 None;完整的请求头能解析,并报告其确切长度,不动后面的载荷 |
truncated_and_malformed_input_never_panics |
protocols/tests/unit/dns/message.rs |
DNS 应答的每个前缀都能解码而不 panic;超出末尾的 rdlength 是错误 |
varint_truncated_at_every_boundary、tcp_response_truncated_at_every_boundary |
protocols/tests/unit/hysteria/protocol.rs |
Hysteria 2 的 varint 和响应在每个截断点上的行为 |
rejects_truncated_metadata |
protocols/tests/unit/mux/frame.rs |
元数据被截断的 mux.cool 帧 |
a_truncated_client_hello_yields_nothing_rather_than_garbage |
protocols/tests/unit/sniff/tls.rs |
在多处被截断的 ClientHello 要么得到真实的 SNI,要么什么也得不到,绝不会得到另一个域名 |
2.0 重构:单一流水线
Section titled “2.0 重构:单一流水线”1.x 版本通过任务和通道搬运字节。2.0 版本用单个任务驱动每条连接,该任务拥有连接的传输层、缓冲区、协议核心以及它打开的每个出站。只有被许多连接共享的承载者才保留自己的驱动任务:承载 gRPC stream 的 HTTP/2 连接、WireGuard 设备,以及 Hysteria 2 的 QUIC 连接。旧流水线被直接删除,没有并存保留,这也是所有 crate 一起升到 2.0.0 的原因。
flowchart LR sock["原始 socket"] --> trans["TcpInboundTransport"] trans --> vc["VcLink:两条传输 Bytes 的 mpsc 通道"] vc --> server["ProxyProtocolServer(tower Service)"] server --> circuit["ServerCircuit"] circuit --> router["Router"] router --> client["ProxyProtocolClient"] client --> out["OutboundTransport 拨号"]
传输层把 socket 变成一个 VcLink,即一对传输 Bytes 帧的有界 mpsc 通道。tower 风格的 ProxyProtocolServer 把握手解码成一个 ServerCircuit,OneOrManyTask 在 JoinSet 中承载派生出来的复制循环,多路复用连接则使用一个无界的接收端 stream。每一跳都是一个任务或一个通道。
flowchart LR client["客户端传输层"] --> rt["ProxyServerRuntime(单个任务)"] rt --> core["ProxyCoreDecode"] core -->|"effect"| rt rt --> conn["Connector"] conn --> direct["到目的地的 TcpStream"] conn --> upstream["基于上游的 ProxyClientRuntime"]
一个 ProxyServerRuntime future 拥有客户端传输层、三个定长缓冲区、sans-I/O 协议核心,以及协议核心打开的每个出站。协议核心是一个从事件到 effect 的纯函数。出站就是 Connector 返回的任何东西;对于上游代理,它是一个 ProxyClientRuntime,在同一个任务中运行客户端 codec。完整的讲解见一条连接的一生。
Etemenanki 历史中迁移的主要步骤,以及此后发布的修复:
| Commit | 步骤 |
|---|---|
20d118b rewrite concepts |
新的 core、runtime、buffer、wake、link 和 relay 模块;旧模块移入 concepts::legacy |
9b7be19 split the proxy server and client traits |
服务端使用 ProxyCoreDecode;客户端使用 ProxyCoreEncode 和 ProxyClientRuntime |
fc40d1c add etemenanki-environment |
作为 Connector 的主机拨号器,以及 vendored 进来的路由模型 |
a5c225b protocols: move the task-and-channel pipeline under legacy |
旧的服务端、客户端和传输层移入 protocols::legacy;共享的编解码代码保持原位 |
8bad424 … 6b33b3c |
逐个协议改成基于切片的解析器和编码器,即新协议核心需要的形态 |
8653d8f pipeline foundations |
Flow、Timing、SniffPrefix、PassthroughCore、TransportStream、InboundTransport、TransportConnector |
95ea530 … 8300195 |
每个协议一个 sans-I/O 协议核心和客户端 codec,然后是 SOCKS、Hysteria 2、WireGuard 和 TUN |
5c826f4 app: serve every inbound and outbound on the new pipeline |
app 完成切换;配置 schema、路由和热重载保持不变 |
44c5109、e79a357 drop the legacy pipeline |
删除 protocols::legacy 和 concepts::legacy;tower 从所有 Etemenanki 清单中移除 |
054cf34 release etemenanki 2.0.0 |
四个 crate 全部为 2.0.0 |
6c727ee wireguard: bound the driver’s per-connection uplink |
WireGuard 驱动对每条连接最多持有一个上行项,并且只在不持有时才读取该连接的通道,因此一条停止消费的连接只会阻塞它自己的生产者,不影响其他连接 |
53eed3b mux: send each downlink frame once, keep a read’s held frames |
每个 mux.cool 下行帧只暂存一次;Demux::feed_chunks 取代 feed_whole,一次调用接收一次 VMess 读取的全部分块 |
2f1f8cb release etemenanki-protocols 2.0.1 |
etemenanki-protocols 为 2.0.1;其他三个 crate 仍为 2.0.0 |
351abcc socks: hold a UDP association to its control connection’s client |
UDP 关联只接收来自控制连接 IP 的数据报,按规范形式比较;端口由第一个转发的数据报,或由一个指明该 IP 及端口的请求固定下来。没有指明确切来源的 Unix socket 客户端,以及收不到其客户端数据报的中继,都会以 0x02 拒绝。SocksUdpLink 也按同样的规范形式比较回复的来源。由 protocols/tests/pipeline/socks.rs 中的 udp_association_* 和 udp_link_* 测试、protocols/tests/unit/socks/server.rs 中的 ExpectedSender 测试,以及 protocols/tests/unit/socks/protocol.rs 中的 endpoint_sees_through_ipv4_mapping_and_ignores_flow_info 锁定 |
596916d release etemenanki-protocols 2.0.2 |
etemenanki-protocols 为 2.0.2;其他三个 crate 仍为 2.0.0 |
至今仍影响代码形态的后果:
- 没有兼容层。
ServerCircuit、VcLink、RoutingKey、OneOrManyTask以及Service形态的服务端和客户端都已删除。1.x 的使用方必须迁移到协议核心和 connector,katanav3.0.0就是这样做的。新代码不要再加回适配层。 - 测试让协议核心与它的 codec 相互对照。 1.x 的对照测试(
*_vs_legacy_*)随旧流水线一起删除。协议测试现在让每个服务端协议核心对接它自己的客户端 codec,既通过CoreHarness手动驱动,也在真实 socket 上运行;与上游的一致性则交给 Xray 和 Hysteria 互操作测试套件。 - 回复要等拨号完成。 代理请求要等它的出站真正连上后才得到应答,连不上时则带着原因被拒绝。唯一的例外是指定了 IP 且需要嗅探的请求:客户端在收到回复前不会发送任何数据,因此 HTTP
CONNECT、SOCKS 和 Hysteria 2 会立即应答这类请求。 - 部分文档注释仍写着 “new pipeline”。 它们指的就是唯一的这条流水线。
| 不变量 | 保证机制 | 由谁固定 |
|---|---|---|
| 依赖只有一个方向:concepts ← environment ← protocols ← app 和 katana | 各个清单;Cargo 拒绝依赖环 | 构建本身 |
etemenanki-concepts 不含协议代码,其协议核心中没有 I/O |
concepts/Cargo.toml 中没有 workspace 依赖;协议核心以 Event 的形式接收字节,并以 Effect 作答 |
core_is_driven_without_any_io(concepts/tests/runtime.rs)、codec_is_driven_without_any_io(concepts/tests/client.rs) |
| 一条被代理的连接在一个任务中运行,即使经由用客户端 codec 访问的上游代理也是如此 | ProxyClientRuntime 是一个 AsyncRead + AsyncWrite 值,由服务端运行时就地轮询。共享承载者(拨号得到的 gRPC 连接、WireGuard 设备、Hysteria 2 连接)运行独立的驱动任务 |
server_runtime_relays_through_a_client_runtime_in_one_task(concepts/tests/client.rs) |
| 没有要求 Hysteria 2 或 TUN 的 crate 两者都得不到 | 可选依赖放在 hysteria 和 tun 之后;没有 default feature |
可在 katana 的 Cargo.lock 中看到:其中有 quinn(它要求了 hysteria),但没有 ipstack 或 tun-rs |
| rustls 从不隐式选择 crypto provider | 两个 tls_config 函数都用 builder_with_provider 指定 ring provider;服务端还固定使用 TLS 1.3 |
没有专门的测试。每个 Hysteria 2 握手测试,例如 protocols/tests/pipeline/hysteria.rs 中的 new_server_vs_new_client_tcp,都会构建这两份配置,但没有测试统一进第二个 provider |
| 协议解析不会因不可信输入而 panic | crate 级的 clippy deny 和 helpers/parse.rs |
对解析器意味着什么一节列出的截断测试 |
| vendored 进来的路由模型保持 harranu 的语义 | routing.rs 原样 vendored |
first_matching_rule_wins、any_matcher_within_a_rule_matches、port_parser_rejects_an_inverted_range 以及 environment/tests/unit/routing.rs 中的其余测试 |
两个仓库的 CI 都不运行测试、格式检查或 clippy。workflow 只构建 release 二进制;下面的验证门槛在修改合入前和每次发布前运行。
| Workflow | 触发条件 | 做什么 |
|---|---|---|
Etemenanki build.yml |
push 到 main、pull request、手动触发 |
安装 pkg-config 和 libssl-dev,然后执行 cargo build --release --package etemenanki-app --target x86_64-unknown-linux-gnu,并上传二进制 |
katana build.yml |
push 到 main 或 master、pull request、手动触发 |
cargo build --release --locked,上传 target/release/katana |
katana release.yml |
v* tag、手动触发 |
用 --locked 构建 x86_64-unknown-linux-gnu 和 x86_64-unknown-linux-musl;如果 readelf 显示动态链接了 libssl 或 libcrypto 则失败,如果 musl 二进制带有程序解释器或任何 NEEDED 条目也失败;在 tag push 时,把两个二进制连同 .sha256 文件发布为 GitHub release |
release.yml 中的动态 OpenSSL 检查,正是 katana 开启 vendored-openssl 的原因:无论主机上的 OpenSSL 是什么版本,二进制都必须能运行。
cargo fmt --all -- --checkcargo test --workspacecargo clippy --workspace --all-targets --all-features -- -D warnings# 发布前还要运行:cargo check --workspace --lockedcargo fmt --all -- --checkcargo test --lockedcargo clippy --all-targets --all-features -- -D warnings# 发布前还要运行:cargo metadata --locked --no-deps --format-version 1cargo check --locked这些确切命令带来的几点后果:
- clippy 门槛中的
--all-features会一次开启所有 feature:etemenanki-environment/quic,因此dial/quic.rs及其测试会被 lint;tun,它的rtnetlink依赖只存在于 Linux target 上;以及vendored-openssl,它从源码构建 OpenSSL,需要perl、make和 C 编译器。 cargo test --workspace会在成员之间统一 feature。 app 在etemenanki-protocols上开启了hysteria和tun,所以 workspace 级的运行也会编译并运行protocols/tests/pipeline.rs中的hysteria和tun模块。单独运行cargo test -p etemenanki-protocols则不会;需要加上--features hysteria,tun。- workspace 测试运行中没有任何东西开启
quic,因为没有成员要求它。QUIC 拨号器测试(environment/tests/integration/quic.rs中的dials_a_quic_server_over_loopback、a_socket_wrap_sees_the_endpoint_socket)只有在cargo test -p etemenanki-environment --features quic时才会运行。 - 互操作测试需要 Go 和 submodule。 没有
go,或者 submodule 没有初始化时,Xray 和 Hysteria 测试会打印SKIP:并判为通过。在这样的机器上全绿并不代表这些测试被执行过;先运行git submodule update --init并安装 Go,再相信结果。 - 有些测试不只需要 Go。
a_routed_connect_is_answered_while_the_app_runs(app/tests/integration/e2e_tun.rs)需要创建 TUN 设备的权限(实际上就是CAP_NET_ADMIN);遇到PermissionDenied时它会打印SKIP:并返回。wireguard_outbound_live_env_tcp(app/tests/integration/e2e_wg.rs)标记为#[ignore],需要ETEMENANKI_WG_*环境变量和网络访问。
单元测试是 <crate>/tests/unit/… 下的文件,被挂载到它们所测试的模块中。etemenanki-environment、etemenanki-protocols、etemenanki-app 和 katana 都采用这种布局;etemenanki-concepts 则把七个小测试内联在 buffer.rs、relay.rs 和 wake.rs 中,并通过公共 API 测试它的运行时。
#[cfg(test)]#[path = "../../tests/unit/trojan/core.rs"]mod tests;它们作为子模块编译,因此可以访问私有项,并且受上文 cfg_attr(test, allow(…)) 的覆盖。集成测试是普通的 Cargo 测试 target:
| Crate | 集成测试 target | 覆盖内容 | 测试函数数 |
|---|---|---|---|
| concepts | tests/runtime.rs、tests/client.rs |
用玩具协议核心和 codec(TinyMux、TinyUdp、TinyCodec 等)测试运行时 |
42,其中 7 个内联在 src/ 中 |
| environment | tests/integration.rs(tcp、udp,以及受 feature 控制的 quic) |
回环地址上的拨号器 | 32,其中 24 个是单元测试 |
| protocols | tests/pipeline.rs(hysteria 和 tun 受各自 feature 控制) |
在真实 socket 上让每个服务端协议核心对接它的客户端 codec;传输层;DoT 和 DoH | 416,其中 360 个是单元测试 |
| app | tests/integration.rs(13 个 e2e_* 模块) |
真实的 etemenanki-app 二进制对接 Xray、Hysteria、WireGuard 对端和 TUN 设备 |
123,其中 61 个是单元测试 |
| katana | tests/integration.rs(xray_interop、hysteria_interop、sniff) |
在假面板后运行的真实 katana 二进制,由 Xray 和 Hysteria 客户端驱动;端到端的 disable_sniffing |
109,其中 96 个是单元测试 |
计数是已核对版本中 #[test] 和 #[tokio::test] 属性的数量。如何编写和运行各类测试见测试。