Workspace and crates
Source files: 202 · checked against 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
The code lives in two private repositories. Etemenanki is a Cargo workspace of four crates: three libraries that together form the proxy kernel, and the standalone etemenanki-app binary built on them. katana is a separate repository whose binary, the panel node agent, takes the three library crates from a private Cargo registry. A third repository, harranu, holds the route model that the kernel now carries a copy of.
This page is the map to read before changing anything. It covers which crate owns what, how features and dependencies flow between crates, what every source file is for, the lints that shape all parsing code, how release 2.0 arrived at its one-pipeline design, and the gates a change has to pass. The pages under Concepts, Environment, Protocols and App then go into each crate.
Repositories
Section titled “Repositories”DirectoryEtemenanki/
- Cargo.toml workspace manifest and shared dependency versions
- Cargo.lock
- .cargo/config.toml declares the private registry
- .github/workflows/build.yml
Directoryconcepts/ etemenanki-concepts
- …
Directoryenvironment/ etemenanki-environment
- …
Directoryprotocols/ etemenanki-protocols
- …
Directoryapp/ etemenanki-app, the standalone binary
- …
DirectoryXray-core/ git submodule, reference and interop tests only
- …
Directoryhysteria/ git submodule, reference and interop tests only
- …
Directorykatana/
- Cargo.toml depends on the three library crates by version
- Cargo.lock
- .cargo/config.toml declares the private registry
Directory.github/workflows/ build.yml and release.yml
- …
Directorysrc/
- …
Directorytests/
- …
DirectoryXboard/ panel reference tree, not built
- …
DirectoryV2bX/ panel reference tree, not built
- …
DirectoryXrayR/ panel reference tree, not built
- …
| Repository | What it builds | Consumes |
|---|---|---|
| Etemenanki | etemenanki-concepts, etemenanki-environment and etemenanki-protocols (libraries, published to a private Cargo registry), and etemenanki-app (binary) |
Only crates.io and its own path dependencies |
| katana | The katana binary |
The three library crates, from the private registry: etemenanki-concepts and etemenanki-environment at 2.0.0, etemenanki-protocols at 2.0.1 |
| harranu | A standalone route-model crate | Nothing in the kernel or katana depends on it any more |
The workspace manifest lists exactly members = ["app", "concepts", "environment", "protocols"]. Everything else in the checkout, including the submodules, is invisible to Cargo.
Reference trees
Section titled “Reference trees”Xray-core/ (upstream XTLS/Xray-core) and hysteria/ (upstream HyNetworks/hysteria) are git submodules declared in .gitmodules. Nothing in them is compiled by Cargo. They serve two purposes:
- Porting reference. Most protocol modules are ports, and their module docs name the Go file they follow:
trojan/protocol.rsportsproxy/trojan/protocol.go,mux/frame.rsportscommon/mux/{frame.go,reader.go,writer.go},hysteria/obfs.rsportshysteria/extras/obfs/salamander.go, andhysteria/PROTOCOL.mdis the specification both Hysteria 2 ends are written against. Shadowsocks 2022 is ported fromsing-shadowsocks/shadowaead_2022, which is not vendored. - Interoperability tests.
app/tests/support/mod.rsbuilds the upstream binaries on first use:go build -o $CARGO_TARGET_TMPDIR/xray ./maininsideXray-core/, andgo buildinsidehysteria/app. The results sit behindLazyLockstatics,XRAY_BINandHYSTERIA_BIN, so each test binary builds each upstream at most once. Whengois missing or the build fails, the helper prints aSKIP:line and returnsNone, and every test that needs the binary returns early and passes. katana’stests/support/mod.rsbuilds the same two binaries from the sibling checkout../Etemenanki/Xray-coreand../Etemenanki/hysteria, and also skips when that directory does not exist.
katana’s Xboard/, V2bX/ and XrayR/ submodules play the same reference role for panel compatibility. Nothing builds them.
Where the route model came from
Section titled “Where the route model came from”The route model (first-match rule table, domain, CIDR and port matchers, GeoIP and GeoSite .dat loading) has moved twice: from local copies in the app and in katana into the shared harranu crate, and from harranu into etemenanki-environment:
| Step | Repository | Commit |
|---|---|---|
The app drops its local copy and depends on the published harranu crate |
Etemenanki | 58ad56c Extract route model into shared harranu crate, drop patch hack |
| katana does the same, the same day | katana | 90c5e35 Replace vendored route model with the shared harranu crate |
harranu 0.3 is vendored, unchanged, as etemenanki_environment::routing |
Etemenanki | fc40d1c add etemenanki-environment: host dialers and the vendored route model |
| katana routes with the kernel’s copy | katana | e9bc640 route with the kernel’s route model instead of harranu |
The reason for the last move is in fc40d1c: depending on harranu from a private registry made the workspace unbuildable wherever that registry was unreachable. The consequence for contributors is that routing changes go into environment/src/routing.rs. A change to harranu reaches neither the app nor katana. The route model itself is described in Routing.
Crate graph
Section titled “Crate graph”Dependencies point one way, from the binaries down to etemenanki-concepts. Each arrow is a dependency declared in a manifest; a label names the features the dependent crate turns on.
flowchart BT concepts["etemenanki-concepts"] environment["etemenanki-environment"] protocols["etemenanki-protocols"] app["etemenanki-app (binary)"] katana["katana (binary, own repository)"] environment --> concepts protocols --> concepts protocols --> environment app --> concepts app --> environment app -->|"hysteria, tun"| protocols katana --> concepts katana --> environment katana -->|"hysteria, vendored-openssl"| protocols
Inside the workspace, each internal dependency carries three keys: a path (for example ../concepts) so a workspace build uses the local source, and a version = "2.0.0" plus a registry naming the private registry, which is what cargo publish writes into the published manifest in place of the path. Every crate also sets publish to that registry alone, so none of them can reach crates.io by accident.
The four crates were released together at 2.0.0 in 054cf34 (“release etemenanki 2.0.0”), and etemenanki-environment was published for the first time in that release, at the same number. Since then only etemenanki-protocols has moved, in two patch releases listed under the 2.0 redesign: 2.0.1 (2f1f8cb, “release etemenanki-protocols 2.0.1”) with the WireGuard and mux fixes, and 2.0.2 (596916d, “release etemenanki-protocols 2.0.2”), which holds a SOCKS5 UDP association to its control connection’s client. 2.0.2 changes no public API: the source a UDP ASSOCIATE declares reaches SocksInbound through the crate-private handshake_with_udp_source, and the public handshake keeps its signature. etemenanki-concepts, etemenanki-environment and etemenanki-app stay at 2.0.0. The internal requirements still read version = "2.0.0", which 2.0.2 satisfies, and the workspace Cargo.lock records etemenanki-protocols at 2.0.2.
Features
Section titled “Features”No crate declares a default feature set, so every feature below is opt-in.
| Feature | Crate | What it turns on | Enabled by |
|---|---|---|---|
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; compiles protocols::hysteria |
etemenanki-app, katana |
tun |
etemenanki-protocols |
dep:ipstack, dep:tun-rs, dep:rtnetlink (a Linux-only target dependency); compiles protocols::tun under #[cfg(all(feature = "tun", unix))] |
etemenanki-app |
vendored-openssl |
etemenanki-protocols |
openssl/vendored: OpenSSL is built from source and linked statically |
katana |
quic |
etemenanki-environment |
dep:quinn; compiles dial::quic (QuicDialer, SocketWrap) |
No crate; only --all-features or an explicit --features quic |
The reasons are recorded in the manifests. hysteria is off by default because it “pulls in a whole second TLS stack (quinn -> rustls)”, and a downstream that only wants the classic protocols must not inherit it on its next version bump. tun is off for the same reason: an interface-management stack should not reach crates that never touch it. katana opts in to hysteria because it serves Hysteria 2 nodes, and to vendored-openssl because its release binaries must not link OpenSSL dynamically (see CI).
protocols::hysteria builds its QUIC endpoints on quinn directly. It does not enable etemenanki-environment/quic, so no production build at the verified revision compiles QuicDialer.
How katana consumes the kernel
Section titled “How katana consumes the kernel”Dependency in katana’s Cargo.toml |
Requirement | Features | Resolved in Cargo.lock |
|---|---|---|---|
etemenanki-concepts |
version = "2.0.0", private registry |
none | 2.0.0, registry source and checksum recorded |
etemenanki-environment |
version = "2.0.0", private registry |
none | 2.0.0 |
etemenanki-protocols |
version = "2.0.0", private registry |
vendored-openssl, hysteria |
2.0.1 |
A bare "2.0.0" is a caret requirement: any 2.x release at or above 2.0.0 satisfies it, which is why etemenanki-protocols resolves to 2.0.1 without a manifest change. katana v3.0.1 has not taken 2.0.2; its lockfile still records 2.0.1. The exact version is pinned by Cargo.lock, which records the registry source and a checksum for each crate, and both CI workflows build with --locked, so a build fails instead of resolving a different kernel. To move katana to a new kernel release, update one package at a time with cargo update -p <crate> --precise <version> and check that nothing else in the lockfile moved.
Both repositories declare the registry in .cargo/config.toml with the cargo:token credential provider. The token is never in the repository; CI passes it in through an environment variable.
Workspace settings
Section titled “Workspace settings”| Setting | Value | Where |
|---|---|---|
| Edition | 2024 |
[workspace.package], inherited by every crate |
| Resolver | 3 |
[workspace] |
| Shared versions | One [workspace.dependencies] table; members write tokio.workspace = true |
Cargo.toml |
| Release profile | lto = true |
Etemenanki Cargo.toml |
| Release profile (katana) | lto = true, codegen-units = 4 |
katana Cargo.toml |
Key types at the crate boundaries
Section titled “Key types at the crate boundaries”Three traits from etemenanki-concepts are the seams every other crate plugs into. The details are on Server core, Server runtime and Links, connectors and net types.
concepts/src/link.rs → Connector and DatagramLink: what an outbound is, and how one is opened.
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: the server side of a protocol as a sans-I/O state machine.
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: the per-connection driver that ties a core, its transport and a connector together.
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 wraps a byte stream, over_datagrams a DatagramLink; both assert BUF_SIZE > Core::STAGING_RESERVE, because a smaller buffer would never have room to read. showing_progress turns the quiet Future into the Stream of Traffic reports described under tokio-stream below.
protocols/src/flow.rs → Flow: the Target of every server core in etemenanki-protocols, and therefore what the app’s and katana’s connectors receive.
pub struct Flow<T> { pub destination: Destination, pub user: NetworkUser<T>, pub sniffed: Option<SniffedBehavior>, pub source: Option<IpAddr>,}Who implements what, at the verified revisions:
| Trait | Implementations |
|---|---|
ProxyCoreDecode |
TrojanCore, VlessCore, VMessCore, ShadowsocksCore, Ss2022Core, HttpCore, PassthroughCore, Hy2StreamCore, Hy2UdpCore, TunUdpCore. SOCKS is the exception: socks/server.rs → SocksInbound drives its own control connection. |
ProxyCoreEncode (client codec) |
TrojanStream, VlessStream, VMessStream, SsStream, Ss2022Stream, SocksConnect, HttpConnect, and NoCodec |
ProxyCoreEncodeDatagram |
TrojanDatagram, VlessDatagram, VMessDatagram, and NoCodec |
Connector<Target> |
concepts: SocketConnector, ProxyClientConnector, every FnMut(Target) -> Fut closure; environment: Dialer (for DialTarget and 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 |
External dependencies
Section titled “External dependencies”| Dependency | Crates | Why it is there |
|---|---|---|
tokio (full) |
all | The runtime, sockets and timers. Every crate’s dev-dependencies add test-util for start_paused tests, because idle and keepalive deadlines are minutes long. |
tokio-stream |
concepts, protocols, app | The Stream trait: in ProxyShowsProgress mode ProxyServerRuntime is a Stream whose items are Result<Traffic, RuntimeError<_>> progress reports, rather than a Future. |
tokio-util |
protocols, app | CancellationToken: the app scopes every task of a generation under one token (serve.rs → spawn_scoped), and the Hysteria 2 and TUN inbounds stop their tasks with one. AbortOnDropHandle ties a background task to the value that owns it: the HTTP/2 driver of a dialed gRPC stream, and the HTTP/3 driver and datagram pump of a Hysteria 2 client connection. |
futures |
protocols | The Sink and Stream traits over a tokio-tungstenite socket (ws/stream.rs), and a Shared future so concurrent callers await one Hysteria 2 connection build (hysteria/slot.rs). |
pin-project |
concepts | Pinned projections in the hand-written futures of relay.rs. |
compact_str, smallvec |
concepts and up | Remote::Domain holds a CompactString; EffectList is a SmallVec with INLINE_EFFECTS = 4 inline slots, so a typical event allocates nothing. |
parking_lot |
concepts, protocols, app | Short, never-awaited locks: wake.rs → ReadyQueue, the user tables, the rebuildable connection slots. |
arc-swap |
protocols, app | User tables that a reload replaces under live listeners, for example vmess/accounts.rs and the Hysteria 2 authenticator. |
rand |
protocols | Salts, request padding, VMess session keys and DNS query IDs. |
openssl, tokio-openssl |
protocols | Every TLS session over TCP: the TLS transport (transports/tls/config.rs builds servers from SslAcceptor::mozilla_intermediate_v5 and sets a minimum of TLS 1.2 on both sides) and the DNS-over-TLS and DNS-over-HTTPS backends in dns/mod.rs. |
quinn, h3, h3-quinn, rustls, rustls-native-certs, rustls-pemfile, rustls-pki-types |
protocols, feature hysteria; quinn alone also in environment, feature quic |
QUIC and HTTP/3 for Hysteria 2, and the QUIC dialer. rustls is there only because it is quinn’s one crypto backend. |
blake2 |
protocols, feature hysteria |
The Salamander obfuscator’s hash. |
boringtun, smoltcp |
protocols (always) | WireGuard: boringtun’s Tunn runs the WireGuard protocol, smoltcp is the userspace TCP/IP stack inside the tunnel. Not behind a feature. |
h2, http |
protocols | h2 carries the gRPC transport over HTTP/2 (transports/grpc/, and serving one HTTP/2 connection as many streams in transports/accept.rs). The http types also serve the WebSocket upgrade headers and the Hysteria 2 HTTP/3 authentication and masquerade. |
tokio-tungstenite |
protocols | The WebSocket transport. |
httparse |
protocols | HTTP/1.x request and response heads in http/protocol.rs. |
ipstack, tun-rs, rtnetlink |
protocols, feature tun |
The userspace IP stack, device creation, and route installation on Linux. |
aes, aes-gcm, chacha20poly1305, hkdf, sha1, sha2, md-5, crc32fast, blake3, subtle |
protocols | The ciphers, KDFs and hashes of VMess, Shadowsocks and Trojan: md-5 for the Shadowsocks EVP_BytesToKey, the VMess command key and the VMess ChaCha20-Poly1305 body key, HKDF-SHA1 for Shadowsocks AEAD subkeys, SHA-256 for the VMess KDF, SHA-224 for the Trojan password hash, CRC32 for the checksum inside a VMess auth id, BLAKE3 for the Shadowsocks 2022 session and identity subkeys. |
socket2 |
environment, protocols | Socket options that must be set between creation and bind or connect (SocketOptions), and TCP keepalive (transports/keepalive.rs). |
cidr, regex, prost |
environment | Route matchers; prost decodes the protobuf in geoip.dat and geosite.dat. The app and katana also use cidr directly. |
moka (future) |
protocols | The DNS answer cache in dns/mod.rs, a moka::future::Cache bounded at CACHE_CAPACITY = 8192 names. |
notify |
app, katana | The configuration-file watcher behind hot reload. |
serde, toml, clap, tracing-subscriber |
app, katana | Configuration, the command line and log output. |
reqwest (native-tls-vendored), serde_json |
katana | The panel HTTP clients and their JSON bodies. On Linux native-tls is OpenSSL, and the vendored flavour builds it statically, like the kernel’s vendored-openssl. |
thiserror, anyhow |
protocols | ProtocolError, and its opaque Other variant. |
uuid |
concepts, protocols, app | UserAuthorization::Uuid for VMess and VLESS users. |
Dev-only dependencies: rcgen, rustls and rustls-pki-types in environment (certificates for the QUIC dialer test), etherparse in protocols (IP packets for the fake TUN device), and openssl, boringtun and smoltcp in the app (test certificates and an in-process WireGuard peer). Besides tokio with test-util, katana’s only dev-dependency is openssl with vendored, for the self-signed certificates of its Xray TLS tests; vendoring it matches the kernel’s vendored-openssl, so both resolve to one OpenSSL build.
Two TLS stacks, one rule
Section titled “Two TLS stacks, one rule”OpenSSL terminates every TLS session that runs over TCP. rustls enters the build only with quinn: under the hysteria feature of etemenanki-protocols, or the quic feature of etemenanki-environment, which no crate enables. It is there because QUIC embeds TLS in its handshake state machine and quinn offers no other backend. Both rustls configurations the code builds, in protocols::hysteria, are built with an explicitly named provider:
let provider = Arc::new(rustls::crypto::ring::default_provider());let builder = rustls::ClientConfig::builder_with_provider(provider)That is hysteria/connection.rs → tls_config for the client and hysteria/server/endpoint.rs → tls_config for the server. The plain ClientConfig::builder() and ServerConfig::builder() panic when a downstream unifies a second crypto-provider feature into the build, so they must never be used. The server configuration also pins TLS 1.3: QuicServerConfig unwraps rustls::quic::ServerConnection::new on the first packet, so a configuration without TLS 1.3 would panic inside the accept loop rather than fail at build time.
Module trees
Section titled “Module trees”Line counts are at the verified revision and include doc comments. Unit tests live outside src/ (see Tests), so the counts are code and documentation only, with one exception: three small inline mod tests blocks in etemenanki-concepts.
etemenanki-concepts
Section titled “etemenanki-concepts”3,889 lines. The sans-I/O model: server cores, client codecs, the two runtimes, links and connectors. It contains no protocol code and depends on no other workspace crate.
| File | Lines | Responsibility |
|---|---|---|
lib.rs |
44 | Crate docs, including the one-task diagram, and the module list |
core.rs |
732 | The protocol traits. Server side: ProxyCoreDecode, Event, Effect, Effects, EffectList. Client side: ProxyCoreEncodeHandshake, ProxyCoreEncode, ProxyCoreEncodeDatagram, Handshake, Reply, Opened, NoCodec |
runtime.rs |
1,394 | ProxyServerRuntime, the Transport trait with StreamTransport and DatagramTransport, Traffic, RuntimeError, the ProxyRunsQuiet and ProxyShowsProgress modes, WORK_BUDGET |
client.rs |
654 | ProxyClientRuntime (a codec over a dialed upstream, exposed as a byte stream or a DatagramLink), ProxyClientConnector, ProxyClientConnecting |
link.rs |
223 | DatagramLink, Outbound, Connector and its closure impl, UdpOutbound, SocketConnector, SocketTarget, NoStream, NoDatagram |
buffer.rs |
269 | ReadBuffer and WriteBuffer, fixed-size boxed arrays that never grow, and Staging, the append-only view of a WriteBuffer’s free tail that a core writes into |
wake.rs |
180 | ReadyQueue and KeyWaker: per-key wake-ups, so the runtime polls only the outbounds that became ready |
relay.rs |
277 | UnidirectionalConnection, BidirectionalConnection, Relayed: byte-copy futures for passthrough paths |
net.rs |
94 | UserAuthorization, NetworkUser, DialNetwork, Remote, Destination |
sniff.rs |
22 | SniffedProtocol, SniffedBehavior, the Sniffer trait |
etemenanki-environment
Section titled “etemenanki-environment”1,363 lines. Two halves that do not depend on each other: how this host opens a socket, and which outbound a flow is routed to.
| File | Lines | Responsibility |
|---|---|---|
lib.rs |
40 | Crate docs and the lint policy |
dial/mod.rs |
100 | DialTarget, Dialer, and its Connector impls for DialTarget and SocketTarget |
dial/socket.rs |
247 | SocketOptions (source address, Interface, packet mark, SocketHook), AddressFamily, per-OS support decided at compile time |
dial/tcp.rs |
109 | TcpDialer, connect_any across addresses, DEFAULT_CONNECT_TIMEOUT = 10 s per attempt |
dial/udp.rs |
187 | UdpDialer, bind_dual, DualStackUdp |
dial/quic.rs |
90 | Feature quic: QuicDialer, SocketWrap |
routing.rs |
590 | The vendored harranu route model: RouteTarget, RouteMatch, RouteTable, DomainRegex, GeoData, .dat protobuf messages |
Dialers are covered in Dialers, routing in Routing.
etemenanki-protocols
Section titled “etemenanki-protocols”23,105 lines. Every protocol is a server core plus a client codec on top of the traits above; transports turn sockets into byte streams; the shared pieces sit beside them.
| Area | Lines | Page |
|---|---|---|
Crate root, core/, helpers/ |
1,487 | Protocol foundations |
sniff/ |
360 | Sniffing |
dns/ |
748 | DNS |
transports/ |
2,356 | TCP and TLS, WebSocket and gRPC |
mux/ |
929 | mux.cool and XUDP |
socks/, http/ |
1,422 and 760 | SOCKS, HTTP |
trojan/, vless/ |
928 and 1,000 | Trojan, VLESS |
vmess/ |
2,876 | VMess crypto, VMess wire |
ss_legacy/, ss_2022/ |
1,334 and 1,668 | Shadowsocks, Shadowsocks 2022 |
hysteria/ (feature hysteria) |
4,739 | Hysteria 2 client, Hysteria 2 server |
wireguard/ |
1,555 | WireGuard |
tun/ (feature tun, Unix) |
943 | TUN |
Shared pieces
Section titled “Shared pieces”| File | Lines | Responsibility |
|---|---|---|
lib.rs |
38 | Lint policy and module list, with the hysteria and tun gates |
flow.rs |
70 | Flow, the target of every core |
error.rs |
72 | ProtocolError and its mapping onto io::ErrorKind |
macros.rs |
23 | byte_newtype!, fixed-size byte newtypes for key material |
core/mod.rs |
464 | FlowKey, SubKey, Phase, Timing, HANDSHAKE_TIMEOUT, RELAY_IDLE_TIMEOUT, SniffPrefix, Passthrough, PassthroughCore |
core/harness.rs |
125 | CoreHarness: a hand-driven runtime stand-in for testing a core without sockets |
helpers/address.rs |
354 | AddressCodec (the type-byte address format of SOCKS5, Trojan, Shadowsocks, VLESS and 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-free slice access (see Lints) |
helpers/mod.rs |
4 | Module list |
sniff/mod.rs |
84 | SNIFF_TIMEOUT, SNIFF_LIMIT, sniff, worth_sniffing, plausible_domain |
sniff/collector.rs |
77 | Collector and Verdict: the clock-free accumulator of a flow’s first bytes |
sniff/tls.rs |
101 | TlsSniffer: ClientHello SNI |
sniff/http.rs |
98 | HttpSniffer: HTTP/1.x Host |
dns/mod.rs |
545 | Resolver, Backend, ResolverSpec, the cache and its TTL bounds |
dns/message.rs |
203 | The minimal A/AAAA wire codec: encode_query, decode_answer |
Transports
Section titled “Transports”| File | Lines | Responsibility |
|---|---|---|
transports/mod.rs |
14 | Module list |
transports/accept.rs |
175 | InboundTransport, Accepted, TRANSPORT_HANDSHAKE_TIMEOUT, serving an HTTP/2 connection as many streams |
transports/connect.rs |
195 | TransportKind, TransportConnector: resolve, connect, wrap an upstream |
transports/stream.rs |
71 | TransportStream, the one byte-stream type every transport yields |
transports/keepalive.rs |
33 | set_keepalive for every accepted or dialed socket |
transports/tls/mod.rs |
8 | Module list |
transports/tls/config.rs |
201 | ServerConfig, ClientConfig, Alpn, VerifyMode over OpenSSL |
transports/tls/stream.rs |
91 | MaybeTlsStream, tcp_from_std, accept_optional_tcp, wrap_optional_tcp |
transports/ws/mod.rs |
7 | Module list |
transports/ws/endpoint.rs |
254 | WsRoute, WsTarget, path and early-data handling, MAX_EARLY_DATA, MAX_WS_MESSAGE_LEN |
transports/ws/stream.rs |
378 | WsStream: WebSocket messages as bytes, WS_IDLE_TIMEOUT, WS_KEEPALIVE_INTERVAL |
transports/grpc/mod.rs |
10 | Module list |
transports/grpc/framing.rs |
320 | encode_hunk, encode_multi_hunk, HunkDecoder |
transports/grpc/settings.rs |
149 | HTTP/2 settings, MAX_GRPC_MESSAGE_LEN, H2_IDLE_TIMEOUT, GrpcPaths, GrpcMode |
transports/grpc/liveness.rs |
145 | Liveness: idle deadline and PING supervision of a served connection |
transports/grpc/stream.rs |
305 | GrpcStream: one HTTP/2 stream as bytes |
mux.cool
Section titled “mux.cool”| File | Lines | Responsibility |
|---|---|---|
mux/mod.rs |
52 | MUX_ADDRESS, MUX_PORT, mux_destination, is_mux_destination |
mux/frame.rs |
407 | The mux.cool frame codec, FrameMeta, SessionStatus, MAX_META_LEN, MAX_DATA_LEN |
mux/demux.rs |
470 | Demux: the server-side demultiplexer shared by the Trojan, VLESS and VMess cores; feed for a plain carrier, feed_chunks for all of a VMess read’s chunks in one call |
SOCKS and HTTP
Section titled “SOCKS and HTTP”| File | Lines | Responsibility |
|---|---|---|
socks/mod.rs |
14 | Module list |
socks/protocol.rs |
249 | SOCKS4, 4a and 5 wire constants and primitives; endpoint, the canonical (IpAddr, u16) both ends compare a relay datagram’s sender by |
socks/handshake.rs |
273 | The server handshake up to the reply: handshake, handshake_with_udp_source (crate-private; also returns the source a UDP ASSOCIATE names), Request, Version, reply writers |
socks/server.rs |
445 | SocksInbound: the bespoke driver that owns the control connection; ExpectedSender, the one client a UDP association hears |
socks/codec.rs |
154 | SocksConnect: the SOCKS5 CONNECT client codec |
socks/udp_link.rs |
207 | SocksUdpLink: a client-side UDP ASSOCIATE as a DatagramLink |
socks/config.rs |
80 | SocksServerConfig, SocksAuth |
http/mod.rs |
11 | Module list |
http/protocol.rs |
283 | Request and response heads, fixed responses, forwarded-request rewriting |
http/core.rs |
322 | HttpCore: CONNECT tunnels and absolute-form forwarding |
http/codec.rs |
96 | HttpConnect: the CONNECT client codec |
http/config.rs |
48 | HttpServerConfig |
Trojan and VLESS
Section titled “Trojan and VLESS”| File | Lines | Responsibility |
|---|---|---|
trojan/mod.rs |
11 | Module list |
trojan/protocol.rs |
332 | Request header and UDP packet framing, password_hash |
trojan/users.rs |
89 | TrojanServerConfig, Validator |
trojan/core.rs |
353 | TrojanCore |
trojan/codec.rs |
143 | TrojanStream, TrojanDatagram |
vless/mod.rs |
15 | Module list |
vless/protocol.rs |
309 | Request and response headers, length-prefixed UDP packets |
vless/validator.rs |
77 | Validator, the user table |
vless/config.rs |
49 | VlessServerConfig |
vless/core.rs |
365 | VlessCore |
vless/codec.rs |
185 | VlessStream, VlessDatagram |
| File | Lines | Responsibility |
|---|---|---|
vmess/mod.rs |
18 | Module list |
vmess/aead.rs |
616 | The HMAC-SHA256 KDF, the AES-128 auth-id codec, the sealed header envelope |
vmess/keys.rs |
211 | Typed 16-byte key material, so a key cannot be passed where an IV is expected |
vmess/accounts.rs |
358 | Account, AccountValidator: matching an auth id to a user |
vmess/protocol.rs |
419 | Request and response header codecs, Security, RequestOptions |
vmess/framing.rs |
388 | ChunkStream, ChunkDecoder: body-chunk framing |
vmess/session.rs |
108 | OutboundSession: per-connection key and IV derivation |
vmess/core.rs |
563 | VMessCore |
vmess/codec.rs |
195 | VMessStream, VMessDatagram |
Shadowsocks
Section titled “Shadowsocks”| File | Lines | Responsibility |
|---|---|---|
ss_legacy/mod.rs |
16 | Module list; supported ciphers |
ss_legacy/aead.rs |
656 | Method, Session, ChunkEncoder, ChunkDecoder, EncryptWriter, DecryptReader |
ss_legacy/users.rs |
97 | ShadowsocksServerConfig, Resolved key table |
ss_legacy/protocol.rs |
9 | The shared ADDR codec |
ss_legacy/core.rs |
433 | ShadowsocksCore |
ss_legacy/codec.rs |
123 | SsStream |
ss_2022/mod.rs |
13 | Module list |
ss_2022/crypto.rs |
523 | The 2022-blake3-* methods, session_key, identity_subkey, StreamAead, ChunkWriter, ChunkReader |
ss_2022/protocol.rs |
381 | Request and response header framing, extended identity headers |
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”| File | Lines | Responsibility |
|---|---|---|
hysteria/mod.rs |
29 | Module list and the shared-connection model |
hysteria/protocol.rs |
821 | QUIC varints, TCPRequest and TCPResponse, padding, UDP messages and defragmentation |
hysteria/auth.rs |
177 | The HTTP/3 authentication exchange |
hysteria/obfs.rs |
341 | Salamander packet obfuscation |
hysteria/quic.rs |
66 | Datagram sending shared by both ends: whole first, fragmented only when the peer refuses the whole message |
hysteria/connection.rs |
594 | Hy2Conn: one authenticated client connection, the rustls client configuration |
hysteria/slot.rs |
235 | ConnSlot: the lazily built, rebuildable shared connection |
hysteria/connector.rs |
216 | Hy2Connector, Hy2Stream, Hy2DatagramLink |
hysteria/config.rs |
79 | Hy2Config, Obfs |
hysteria/server/mod.rs |
12 | Module list |
hysteria/server/inbound.rs |
682 | Hy2Inbound, Hy2StreamCore |
hysteria/server/shim.rs |
525 | Splitting one QUIC connection between HTTP/3 and proxy streams |
hysteria/server/datagrams.rs |
357 | QuicDatagrams, Hy2UdpCore |
hysteria/server/authenticator.rs |
216 | Authenticator: which user a credential belongs to |
hysteria/server/endpoint.rs |
131 | The server QUIC endpoint and its rustls configuration |
hysteria/server/io.rs |
114 | QuicIo: h3 stream types as AsyncRead and AsyncWrite |
hysteria/server/masquerade.rs |
82 | Masquerade: the answer given to anyone who is not a client |
hysteria/server/config.rs |
62 | ServerConfig, ListenerConfig |
WireGuard and TUN
Section titled “WireGuard and TUN”| File | Lines | Responsibility |
|---|---|---|
wireguard/mod.rs |
41 | Module docs: why WireGuard is an outbound only |
wireguard/config.rs |
97 | WgConfig, parse_key |
wireguard/device.rs |
998 | WgDevice: the single driver task owning boringtun’s Tunn, the smoltcp stack and the peer socket; at most one uplink item held per connection, CHANNEL_CAP = 256 |
wireguard/connector.rs |
268 | WgConnector, WgStream, WgDatagramLink |
wireguard/slot.rs |
151 | DeviceSlot: the lazily built, rebuildable tunnel |
tun/mod.rs |
42 | Module docs: what is dropped, keeping the outbound path off the device |
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 | TunInbound over ipstack |
tun/udp.rs |
286 | TunUdpLink, TunUdpCore |
tun/tracked.rs |
93 | TrackedTcp: counted streams, so shutdown can wait for them |
etemenanki-app
Section titled “etemenanki-app”4,302 lines. A binary crate: no public API, every module private to main.rs.
| File | Lines | Responsibility |
|---|---|---|
main.rs |
149 | CLI (-c/--config, --test), tracing setup, the notify watcher with a 200 ms debounce, SIGINT/SIGTERM |
config.rs |
666 | The TOML schema (deny_unknown_fields throughout), load, parse_bytes, reload diffing |
instance.rs |
391 | Instance, build, Built: generations and hot reload |
serve.rs |
413 | spawn_scoped, StreamListener, accept loops, run_hysteria_inbound |
connector.rs |
39 | AppConnector: routes a flow and opens it on the chosen outbound |
flow.rs |
22 | Flow, FlowContext |
router.rs |
158 | build_router, route_target: compiling [[route.rule]] into a RouteTable |
transport.rs |
156 | tls_layer, resolve_stream, reject_stream_settings: stream-settings validation shared by both builders |
balancer.rs |
188 | Balancer, Member, Strategy: health-probed outbound groups |
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, the per-protocol client wiring |
outbound/proxy.rs |
215 | ProxyClient, OutboundStream, OutboundDatagram, BlackholeLink |
outbound/freedom.rs |
163 | FreedomConnector, ResolvingUdp |
outbound/udp_fanout.rs |
185 | FanOutLink: per-packet UDP routing, MAX_SUBS |
The app’s internals are on Build pipeline, Serving, Outbounds and Generations and reload.
katana
Section titled “katana”6,460 lines, one binary crate. The details are on the katana overview.
| File | Lines | Responsibility |
|---|---|---|
main.rs |
54 | CLI (-c/--config, --test) |
config.rs |
324 | The TOML schema |
runtime.rs |
439 | The process root: outbound pool, one NodeManager per [[node]], config watching; a reload builds every added node before touching a running one |
api/mod.rs |
369 | PanelClient over the two panel types, panel_node_type |
api/sspanel.rs |
576 | The sspanel (mod_mu) client |
api/newv2board.rs |
452 | The newV2board (UniProxy) client |
manager/mod.rs |
97 | The NodeManager → TransportManager → ProxyManager tree |
manager/node.rs |
675 | Per-node sync and change classification; bootstrap retried with a backoff from 1 s to 60 s; a panel client rebuilt for a [node.api] edit |
manager/transport.rs |
177 | A bound listener and what serves it |
manager/proxy.rs |
239 | A node’s user tables and admission |
inbound.rs |
548 | Inbound construction per panel node type |
serve.rs |
447 | Accept loop and one runtime per connection |
connector.rs |
361 | KatanaConnector: admission, routing and audit for every flow |
router.rs |
120 | Compiling [[route.rule]] against etemenanki_environment::routing |
rule.rs |
76 | Destination-audit rules |
traffic.rs |
472 | Per-user traffic counters and speed limits |
meter.rs |
141 | Metering on the outbound side of a flow |
outbound/mod.rs |
533 | The outbound pool |
outbound/proxy.rs |
179 | Proxy clients for upstream outbounds |
outbound/freedom.rs |
181 | The direct outbound |
Crate-wide lints
Section titled “Crate-wide lints”environment/src/lib.rs and protocols/src/lib.rs open with the same attribute:
#![deny( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::arithmetic_side_effects)]// Tests exercise known-good inputs and may use the panicking forms freely.#![cfg_attr( test, allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::arithmetic_side_effects ))]Together these four lints forbid the usual ways a byte from the network could panic the process: .unwrap() and .expect(), buf[i] and buf[a..b], and any integer operation that can overflow or panic, + on a usize included. They do not cover every panic (an explicit assert! or unreachable! still compiles), so they are a floor, not a proof. etemenanki-concepts, etemenanki-app and katana do not carry the attribute.
The only local exceptions are eight functions in etemenanki-protocols that re-allow clippy::arithmetic_side_effects alone, each with a reason that states the bound:
| Function | File | Stated reason |
|---|---|---|
put_varint, read_varint |
transports/grpc/framing.rs |
A shift by a constant 7 on u64; a shift amount kept below 64 |
read_varint, read_varint_slice |
hysteria/protocol.rs |
The QUIC varint prefix caps the fold at 62 bits |
decode_varint |
hysteria/server/shim.rs |
The same 62-bit cap |
deadline |
transports/grpc/liveness.rs |
An Instant only overflows past the end of the monotonic clock |
after |
transports/ws/stream.rs, socks/server.rs |
The same Instant argument |
A new exception follows the same form: one small function, one lint, and a reason a reviewer can check.
The cfg_attr(test, …) half matters because of how unit tests are compiled: they are mounted into the module they test (see Tests), so they are part of the crate under cfg(test) and would otherwise inherit the ban.
What it means for a parser
Section titled “What it means for a parser”A parser in etemenanki-protocols takes a byte slice that is possibly incomplete and certainly untrusted, and has three outcomes: a value with the number of bytes it used, “not enough bytes yet”, or an error. helpers/parse.rs supplies the building blocks:
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>>;takeisdata.get(index)with a short read mapped toProtocolError::Truncated(what).take_arraycomputes the end offset withchecked_add(overflow becomesProtocolError::Overflow("field offset")) and copies a fixed-size array, which is whatu16::from_be_bytesand friends need.need_moreturns a truncation, which reachesio::ErrorasUnexpectedEof, intoOk(None), and leaves every other error an error. It is how a parser handed a growing buffer says “ask me again with more bytes”.
vless/protocol.rs → parse_request_header shows the resulting style. Single bytes come from first() and get(), fixed fields are checked as soon as they are available, and offsets are computed with saturating_add or 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); }; // ... addons length, command, then the address via `take(buf, 19.., "vless address")`}The errors come from error.rs → ProtocolError, which classifies what went wrong. Its From<ProtocolError> for io::Error impl fixes the io::ErrorKind that callers above the protocol layer see:
| Variant | Meaning | io::ErrorKind |
|---|---|---|
Truncated(&'static str) |
Input shorter than the protocol requires | UnexpectedEof |
Overflow(&'static str) |
Arithmetic over an untrusted length or offset overflowed | InvalidData |
Malformed(&'static str) |
A field held a value the protocol does not permit | InvalidData |
Unsupported(&'static str) |
A feature this implementation does not provide | InvalidData |
Unauthenticated(&'static str) |
The peer could not be authenticated | PermissionDenied |
Crypto(&'static str) |
An AEAD or key-derivation step failed | Other |
Io(io::Error) |
A lower-level I/O failure | passed through unchanged |
Other(anyhow::Error) |
Anything that cannot be classified | Other |
The UnexpectedEof mapping is load-bearing: need_more relies on it to tell “wait” from “reject”.
Parser tests follow a common pattern: encode a valid message, cut it short at many points, and assert that nothing panics and that each prefix is either “need more” or an error. Examples:
| Test | File | Pins |
|---|---|---|
request_header_is_parsed_from_a_slice_once_whole |
protocols/tests/unit/vless/protocol.rs |
A VLESS header cut at its field boundaries, inside the address, and one byte short is None; the whole header parses and reports its exact length, leaving trailing payload alone |
truncated_and_malformed_input_never_panics |
protocols/tests/unit/dns/message.rs |
Every prefix of a DNS answer decodes without panicking; an rdlength past the end is an error |
varint_truncated_at_every_boundary, tcp_response_truncated_at_every_boundary |
protocols/tests/unit/hysteria/protocol.rs |
Hysteria 2 varints and responses at every truncation point |
rejects_truncated_metadata |
protocols/tests/unit/mux/frame.rs |
A mux.cool frame whose metadata is cut short |
a_truncated_client_hello_yields_nothing_rather_than_garbage |
protocols/tests/unit/sniff/tls.rs |
A ClientHello cut at several points yields the real SNI or nothing, never a different name |
The 2.0 redesign: one pipeline
Section titled “The 2.0 redesign: one pipeline”Release 1.x moved bytes through tasks and channels. Release 2.0 drives each connection from a single task that owns the connection’s transport, buffers, protocol core and every outbound it opens. Only carriers shared by many connections keep driver tasks of their own: an HTTP/2 connection carrying gRPC streams, a WireGuard device, and a Hysteria 2 QUIC connection. The old pipeline was removed, not kept alongside, which is why every crate went to 2.0.0 together.
flowchart LR sock["raw socket"] --> trans["TcpInboundTransport"] trans --> vc["VcLink: two mpsc channels of Bytes"] vc --> server["ProxyProtocolServer (tower Service)"] server --> circuit["ServerCircuit"] circuit --> router["Router"] router --> client["ProxyProtocolClient"] client --> out["OutboundTransport dial"]
A transport turned the socket into a VcLink, a pair of bounded mpsc channels carrying Bytes frames. A tower-style ProxyProtocolServer decoded the handshake into a ServerCircuit, and OneOrManyTask carried the spawned copy loops in JoinSets, with an unbounded receiver stream for multiplexed connections. Every hop was a task or a channel.
flowchart LR client["client transport"] --> rt["ProxyServerRuntime (one task)"] rt --> core["ProxyCoreDecode"] core -->|"effects"| rt rt --> conn["Connector"] conn --> direct["TcpStream to destination"] conn --> upstream["ProxyClientRuntime over an upstream"]
One ProxyServerRuntime future owns the client transport, three fixed buffers, the sans-I/O core and every outbound the core opens. The core is a pure function from events to effects. An outbound is whatever a Connector returns; for an upstream proxy that is a ProxyClientRuntime, which runs a client codec inside the same task. The full walk-through is Connection lifecycle.
The main steps of the migration, and the fixes released since, in the Etemenanki history:
| Commit | Step |
|---|---|
20d118b rewrite concepts |
The new core, runtime, buffer, wake, link and relay modules; the old ones move to concepts::legacy |
9b7be19 split the proxy server and client traits |
ProxyCoreDecode for servers; ProxyCoreEncode and ProxyClientRuntime for clients |
fc40d1c add etemenanki-environment |
Host dialers as Connectors, and the vendored route model |
a5c225b protocols: move the task-and-channel pipeline under legacy |
Old servers, clients and transports move to protocols::legacy; shared codec code stays in place |
8bad424 … 6b33b3c |
Slice parsers and encoders per protocol, the shape the new cores need |
8653d8f pipeline foundations |
Flow, Timing, SniffPrefix, PassthroughCore, TransportStream, InboundTransport, TransportConnector |
95ea530 … 8300195 |
A sans-I/O core and client codec per protocol, then SOCKS, Hysteria 2, WireGuard and TUN |
5c826f4 app: serve every inbound and outbound on the new pipeline |
The app switches over; configuration schema, routing and hot reload are unchanged |
44c5109, e79a357 drop the legacy pipeline |
protocols::legacy and concepts::legacy are deleted; tower leaves every Etemenanki manifest |
054cf34 release etemenanki 2.0.0 |
All four crates at 2.0.0 |
6c727ee wireguard: bound the driver’s per-connection uplink |
The WireGuard driver holds at most one uplink item per connection and reads a connection’s channel only while it holds none, so a connection that stops draining blocks its own producer and no other |
53eed3b mux: send each downlink frame once, keep a read’s held frames |
Each mux.cool downlink frame is staged once; Demux::feed_chunks replaces feed_whole and takes every chunk of a VMess read in one call |
2f1f8cb release etemenanki-protocols 2.0.1 |
etemenanki-protocols at 2.0.1; the other three crates stay at 2.0.0 |
351abcc socks: hold a UDP association to its control connection’s client |
A UDP association hears only the control connection’s IP, compared in canonical form, and is pinned to one port by its first forwarded datagram or by a request naming that IP with a port. A Unix-socket client that does not name its exact source, and a relay that cannot hear its client, are refused with 0x02. SocksUdpLink compares reply sources in the same canonical form. Pinned by the udp_association_* and udp_link_* tests in protocols/tests/pipeline/socks.rs, the ExpectedSender tests in protocols/tests/unit/socks/server.rs, and endpoint_sees_through_ipv4_mapping_and_ignores_flow_info in protocols/tests/unit/socks/protocol.rs |
596916d release etemenanki-protocols 2.0.2 |
etemenanki-protocols at 2.0.2; the other three crates stay at 2.0.0 |
Consequences that still shape the code:
- No compatibility layer.
ServerCircuit,VcLink,RoutingKey,OneOrManyTaskand theService-shaped servers and clients are gone. A 1.x consumer had to move to cores and connectors, which is what katanav3.0.0did. New code does not add adapters back. - Tests compare a core with its codec. The 1.x oracle tests (
*_vs_legacy_*) went with the old pipeline. The protocol tests now run each server core against its own client codec, both by hand throughCoreHarnessand over real sockets, and leave agreement with upstream to the Xray and Hysteria interop suites. - Replies wait for the dial. A proxy request is answered once its outbound is actually connected, or refused with a reason when it is not. The one exception is a request that names an IP and is sniffed: the client sends nothing before the reply, so HTTP
CONNECT, SOCKS and Hysteria 2 answer such a request at once. - Some doc comments still say “new pipeline”. They mean the only pipeline.
Invariants
Section titled “Invariants”| Invariant | Enforced by | Pinned by |
|---|---|---|
| Dependencies point one way: concepts ← environment ← protocols ← app and katana | The manifests; Cargo rejects a dependency cycle | The build itself |
etemenanki-concepts has no protocol code and no I/O in its cores |
No workspace dependency in concepts/Cargo.toml; cores receive bytes as Events and answer with Effects |
core_is_driven_without_any_io (concepts/tests/runtime.rs), codec_is_driven_without_any_io (concepts/tests/client.rs) |
| A proxied connection runs in one task, even through an upstream proxy spoken by a client codec | ProxyClientRuntime is an AsyncRead + AsyncWrite value the server runtime polls in place. Shared carriers (a dialed gRPC connection, the WireGuard device, a Hysteria 2 connection) run separate driver tasks |
server_runtime_relays_through_a_client_runtime_in_one_task (concepts/tests/client.rs) |
| A crate that does not ask for Hysteria 2 or TUN gets neither | Optional dependencies behind hysteria and tun; no default features |
Visible in katana’s Cargo.lock, which has quinn (it asks for hysteria) but no ipstack or tun-rs |
| rustls never picks a crypto provider implicitly | builder_with_provider with the ring provider in both tls_config functions; the server also pins TLS 1.3 |
No dedicated test. Every Hysteria 2 handshake test, such as new_server_vs_new_client_tcp in protocols/tests/pipeline/hysteria.rs, builds both configurations, but no test unifies a second provider |
| Protocol parsing cannot panic on untrusted input | The crate-wide clippy denies and helpers/parse.rs |
The truncation tests listed under Lints |
| The vendored route model keeps harranu’s semantics | routing.rs is vendored unchanged |
first_matching_rule_wins, any_matcher_within_a_rule_matches, port_parser_rejects_an_inverted_range and the rest of environment/tests/unit/routing.rs |
Neither repository’s CI runs tests, formatting or clippy. The workflows build release binaries; the validation gates below are run before a change lands and before every release.
| Workflow | Trigger | What it does |
|---|---|---|
Etemenanki build.yml |
Push to main, pull requests, manual |
Installs pkg-config and libssl-dev, then cargo build --release --package etemenanki-app --target x86_64-unknown-linux-gnu, and uploads the binary |
katana build.yml |
Push to main or master, pull requests, manual |
cargo build --release --locked, uploads target/release/katana |
katana release.yml |
Tags v*, manual |
Builds x86_64-unknown-linux-gnu and x86_64-unknown-linux-musl with --locked; fails if readelf shows a dynamic libssl or libcrypto, and fails if the musl binary has a program interpreter or any NEEDED entry; on a tag push, publishes both binaries with .sha256 files as a GitHub release |
The dynamic-OpenSSL check in release.yml is why katana enables vendored-openssl: the binaries must run on hosts whatever their OpenSSL version.
Validation gates
Section titled “Validation gates”cargo fmt --all -- --checkcargo test --workspacecargo clippy --workspace --all-targets --all-features -- -D warnings# before a release, additionally:cargo check --workspace --lockedcargo fmt --all -- --checkcargo test --lockedcargo clippy --all-targets --all-features -- -D warnings# before a release, additionally:cargo metadata --locked --no-deps --format-version 1cargo check --lockedA few consequences of these exact commands:
--all-featuresin the clippy gate turns on everything at once:etemenanki-environment/quic, sodial/quic.rsand its test are linted;tun, whosertnetlinkdependency exists only on Linux targets; andvendored-openssl, which builds OpenSSL from source and needsperl,makeand a C compiler.cargo test --workspaceunifies features across members. The app enableshysteriaandtunonetemenanki-protocols, so the workspace run also compiles and runs thehysteriaandtunmodules ofprotocols/tests/pipeline.rs.cargo test -p etemenanki-protocolsalone does not; add--features hysteria,tun.- Nothing enables
quicin a workspace test run, because no member asks for it. The QUIC dialer tests (dials_a_quic_server_over_loopback,a_socket_wrap_sees_the_endpoint_socketinenvironment/tests/integration/quic.rs) run only withcargo test -p etemenanki-environment --features quic. - Interop tests need Go and the submodules. Without
go, or with an uninitialised submodule, the Xray and Hysteria tests printSKIP:and pass. A green run on such a machine has not exercised them; rungit submodule update --initand install Go before trusting it. - Some tests need more than Go.
a_routed_connect_is_answered_while_the_app_runs(app/tests/integration/e2e_tun.rs) needs permission to create a TUN device (in practiceCAP_NET_ADMIN); onPermissionDeniedit printsSKIP:and returns.wireguard_outbound_live_env_tcp(app/tests/integration/e2e_wg.rs) is#[ignore]and needsETEMENANKI_WG_*environment variables and network access.
Unit tests are files under <crate>/tests/unit/…, mounted into the module they test. etemenanki-environment, etemenanki-protocols, etemenanki-app and katana all follow this layout; etemenanki-concepts instead keeps seven small tests inline in buffer.rs, relay.rs and wake.rs, and tests its runtimes through their public API.
#[cfg(test)]#[path = "../../tests/unit/trojan/core.rs"]mod tests;They compile as a child module, so they can reach private items, and they are covered by the cfg_attr(test, allow(…)) above. Integration tests are ordinary Cargo test targets:
| Crate | Integration target | Covers | Test functions |
|---|---|---|---|
| concepts | tests/runtime.rs, tests/client.rs |
The runtimes with toy cores and codecs (TinyMux, TinyUdp, TinyCodec and others) |
42, of which 7 inline in src/ |
| environment | tests/integration.rs (tcp, udp, and quic behind the feature) |
Dialers on loopback | 32, of which 24 unit |
| protocols | tests/pipeline.rs (hysteria and tun behind their features) |
Each server core against its client codec over real sockets; transports; DoT and DoH | 416, of which 360 unit |
| app | tests/integration.rs (13 e2e_* modules) |
The real etemenanki-app binary against Xray, Hysteria, a WireGuard peer and a TUN device |
123, of which 61 unit |
| katana | tests/integration.rs (xray_interop, hysteria_interop, sniff) |
The real katana binary behind a fake panel, driven by Xray and Hysteria clients; disable_sniffing end to end |
109, of which 96 unit |
Counts are #[test] and #[tokio::test] attributes at the verified revision. How to write and run each kind is on Testing.