限制、超时与内存
源码文件:108 个 · 核对版本 Etemenanki 596916d
Etemenanki/concepts/src/core.rsEtemenanki/concepts/src/runtime.rsEtemenanki/concepts/src/client.rsEtemenanki/concepts/src/buffer.rsEtemenanki/concepts/src/relay.rsEtemenanki/concepts/src/wake.rsEtemenanki/protocols/src/core/mod.rsEtemenanki/protocols/src/core/harness.rsEtemenanki/protocols/src/sniff/mod.rsEtemenanki/protocols/src/sniff/collector.rsEtemenanki/protocols/src/transports/accept.rsEtemenanki/protocols/src/transports/connect.rsEtemenanki/protocols/src/transports/keepalive.rsEtemenanki/protocols/src/transports/ws/endpoint.rsEtemenanki/protocols/src/transports/ws/stream.rsEtemenanki/protocols/src/transports/grpc/settings.rsEtemenanki/protocols/src/transports/grpc/liveness.rsEtemenanki/protocols/src/transports/grpc/framing.rsEtemenanki/environment/src/dial/tcp.rsEtemenanki/environment/src/dial/socket.rsEtemenanki/protocols/src/mux/demux.rsEtemenanki/protocols/src/mux/frame.rsEtemenanki/protocols/src/helpers/address.rsEtemenanki/protocols/src/http/protocol.rsEtemenanki/protocols/src/http/core.rsEtemenanki/protocols/src/http/codec.rsEtemenanki/protocols/src/socks/server.rsEtemenanki/protocols/src/socks/protocol.rsEtemenanki/protocols/src/socks/codec.rsEtemenanki/protocols/src/socks/udp_link.rsEtemenanki/protocols/src/trojan/protocol.rsEtemenanki/protocols/src/trojan/core.rsEtemenanki/protocols/src/trojan/codec.rsEtemenanki/protocols/src/vless/protocol.rsEtemenanki/protocols/src/vless/core.rsEtemenanki/protocols/src/vless/codec.rsEtemenanki/protocols/src/vmess/core.rsEtemenanki/protocols/src/vmess/codec.rsEtemenanki/protocols/src/vmess/framing.rsEtemenanki/protocols/src/vmess/accounts.rsEtemenanki/protocols/src/ss_legacy/aead.rsEtemenanki/protocols/src/ss_legacy/core.rsEtemenanki/protocols/src/ss_legacy/codec.rsEtemenanki/protocols/src/ss_2022/crypto.rsEtemenanki/protocols/src/ss_2022/protocol.rsEtemenanki/protocols/src/ss_2022/core.rsEtemenanki/protocols/src/ss_2022/codec.rsEtemenanki/protocols/src/hysteria/config.rsEtemenanki/protocols/src/hysteria/protocol.rsEtemenanki/protocols/src/hysteria/connection.rsEtemenanki/protocols/src/hysteria/slot.rsEtemenanki/protocols/src/hysteria/server/config.rsEtemenanki/protocols/src/hysteria/server/endpoint.rsEtemenanki/protocols/src/hysteria/server/inbound.rsEtemenanki/protocols/src/hysteria/server/datagrams.rsEtemenanki/protocols/src/hysteria/server/shim.rsEtemenanki/protocols/src/hysteria/obfs.rsEtemenanki/protocols/src/wireguard/config.rsEtemenanki/protocols/src/wireguard/device.rsEtemenanki/protocols/src/wireguard/slot.rsEtemenanki/protocols/src/tun/config.rsEtemenanki/protocols/src/tun/inbound.rsEtemenanki/protocols/src/tun/udp.rsEtemenanki/protocols/src/dns/mod.rsEtemenanki/protocols/src/dns/message.rsEtemenanki/app/src/serve.rsEtemenanki/app/src/connector.rsEtemenanki/app/src/outbound/mod.rsEtemenanki/app/src/outbound/proxy.rsEtemenanki/app/src/outbound/freedom.rsEtemenanki/app/src/outbound/udp_fanout.rsEtemenanki/app/src/balancer.rsEtemenanki/app/src/instance.rsEtemenanki/app/src/inbound/mod.rsEtemenanki/app/src/inbound/tun.rsEtemenanki/app/src/config.rsEtemenanki/app/src/main.rsEtemenanki/concepts/tests/runtime.rsEtemenanki/concepts/tests/client.rsEtemenanki/environment/tests/integration/tcp.rsEtemenanki/protocols/tests/unit/core/mod.rsEtemenanki/protocols/tests/unit/sniff/collector.rsEtemenanki/protocols/tests/unit/transports/accept.rsEtemenanki/protocols/tests/unit/transports/grpc_framing.rsEtemenanki/protocols/tests/unit/transports/grpc_liveness.rsEtemenanki/protocols/tests/unit/transports/ws_endpoint.rsEtemenanki/protocols/tests/unit/mux/demux.rsEtemenanki/protocols/tests/unit/mux/frame.rsEtemenanki/protocols/tests/unit/http/core.rsEtemenanki/protocols/tests/unit/socks/server.rsEtemenanki/protocols/tests/pipeline/socks.rsEtemenanki/protocols/tests/unit/trojan/protocol.rsEtemenanki/protocols/tests/unit/trojan/core.rsEtemenanki/protocols/tests/unit/vmess/accounts.rsEtemenanki/protocols/tests/unit/hysteria/protocol.rsEtemenanki/protocols/tests/unit/hysteria/slot.rsEtemenanki/protocols/tests/unit/hysteria/server/datagrams.rsEtemenanki/protocols/tests/unit/wireguard/slot.rsEtemenanki/protocols/tests/unit/wireguard/device.rsEtemenanki/protocols/tests/pipeline/wireguard.rsEtemenanki/protocols/tests/unit/tun/udp.rsEtemenanki/protocols/tests/unit/dns/mod.rsEtemenanki/app/tests/unit/inbound.rsEtemenanki/app/tests/unit/outbound.rsEtemenanki/app/tests/unit/serve.rsEtemenanki/app/tests/unit/balancer.rsEtemenanki/app/tests/integration/e2e_udp_route.rsEtemenanki/app/tests/integration/e2e_balancer.rs
本页列出 Etemenanki 内核 crate 与 etemenanki-app 中的每一个超时、上限和固定缓冲区大小。对每一项,都给出它的 Rust 名称、取值、定义它的文件以及执行它的机制。后半部分把这些缓冲区常量换算成每连接内存预算:对每个服务端协议核心和每个客户端 codec,一条被代理的连接要占用多少内存。
修改某个限制、添加协议,或者想弄清一条连接为何结束之前,请先读本页。运行时、协议核心、传输层、Hysteria 2、WireGuard 和 TUN 各有自己的页面,本页只讲数值及其作用位置。运维人员能在配置文件中设置的少数几项限制,在面向运维的 限制 一页中说明。
代码中的限制有三种形式:
| 形式 | 所在位置 | 谁能修改 |
|---|---|---|
内核或 app 模块中的 const |
紧挨着执行它的代码,通常是 pub 或 pub(crate),以便测试能引用它的名字 |
只能改代码并发布新版本 |
| 配置默认值 | app/src/ 中的构建函数在某个键缺省时使用的 DEFAULT_* 常量 |
运维人员,通过 可配置的限制 中列出的键 |
| 派生上界 | 基于其他常量的 const fn 或 const 表达式,例如 downlink_overhead(Self::BUF_SIZE) |
只能通过修改它的输入 |
有两条规则保证缓冲区数值前后一致,二者都由代码强制:
- 运行时的字节缓冲区是固定大小的堆上数组(
Box<[u8; N]>,来自concepts/src/buffer.rs中的boxed_array),在构建运行时时一次性分配并清零,之后永不增长。背压来自缓冲区被填满,而不是来自队列。 - 如果缓冲区容纳不下协议核心或 codec 的 staging 预留量,运行时会拒绝构建。
ProxyServerRuntime::build断言BUF_SIZE > Core::STAGING_RESERVE,ProxyClientRuntime::new断言BUF_SIZE > Codec::STAGING_RESERVE。二者都会 panic,信息分别是BUF_SIZE must exceed the core's STAGING_RESERVE or nothing is ever read和BUF_SIZE must exceed the codec's STAGING_RESERVE or nothing can be sealed。
缓冲区大小与预留量
Section titled “缓冲区大小与预留量”BUF_SIZE 是两个运行时的 const 泛型参数:
pub struct ProxyServerRuntime<const BUF_SIZE: usize, Core, Trans, Conn, Mode = ProxyRunsQuiet>where Core: ProxyCoreDecode, Conn: Connector<Core::Target>,pub struct ProxyClientRuntime<const BUF_SIZE: usize, Codec, Conn>where Codec: ProxyCoreEncodeHandshake, Conn: Connector<Codec::Target>,
impl<const BUF_SIZE: usize, Codec, Conn> ProxyClientRuntime<BUF_SIZE, Codec, Conn>where Codec: ProxyCoreEncodeHandshake, Codec::Error: Error + Send + Sync + 'static, Conn: Connector<Codec::Target>,{ pub fn new(codec: Codec, connector: &mut Conn, target: Codec::Target) -> Self}BUF_SIZE 不是 trait 项。每个服务端协议核心都把它作为固有常量暴露出来,例如 impl<T> TrojanCore<T> 上的 pub const BUF_SIZE: usize = 16 * 1024。调用方用它来实例化运行时,如 app/src/serve.rs 中的 drive::<{ TrojanCore::<()>::BUF_SIZE }, _, _>。客户端缓冲区大小由 app 在 app/src/outbound/mod.rs 中选定,作为 ProxyClient<const BUF: usize, S, D> 的第一个参数。
两个预留量常量是 trait 项:
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] { &[] }}
pub trait ProxyCoreEncodeHandshake { type Target; type Error;
const STAGING_RESERVE: usize;
fn start(&mut self, out: &mut Staging<'_>) -> Result<Handshake, Self::Error>; fn reply(&mut self, wire: &mut [u8], out: &mut Staging<'_>) -> Result<Reply, Self::Error>; fn finish(&mut self, out: &mut Staging<'_>) -> Result<(), Self::Error>;}- 服务端协议核心上的
STAGING_RESERVE,是核心处理一个事件时最多向传输层 stage 的字节数,不含该事件自身的载荷。 - 客户端 codec 上的
STAGING_RESERVE,是一次start、reply、finish或 seal 调用在其接收的明文之外最多额外增加的字节数:头部、长度前缀、tag 和填充。 MAX_DATAGRAM是服务端运行时任一侧能完整交付的最大数据包。更长的数据包会被截断,与内核recv的截断方式相同。
运行时把后两者换算成它从数据报出站读取的最大数据报:
impl<const BUF_SIZE: usize, Core, Trans, Conn, Mode> ProxyServerRuntime<BUF_SIZE, Core, Trans, Conn, Mode>where Core: ProxyCoreDecode, Trans: Transport<Addr = Core::TransportAddr>, Conn: Connector<Core::Target>, Conn::Datagram: DatagramLink<Addr = Destination>,{ pub const fn datagram_limit() -> usize}datagram_limit() 返回 min(Core::MAX_DATAGRAM, BUF_SIZE - Core::STAGING_RESERVE)。
Mux 承载连接用一个共享的辅助函数确定预留量,保证一次出站读取拆分出的 Keep 头部总能放得下:
pub const fn downlink_overhead(read_size: usize) -> usize它返回 read_size.div_ceil(MAX_DATA_LEN).saturating_mul(FRAME_OVERHEAD_MAX)。
每个协议核心都把它唯一的截止时间保存在 protocols/src/core/mod.rs 的一个 Timing 值中:
pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);pub const RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
impl Timing { pub fn new() -> Self pub fn phase(&self) -> Phase pub fn is_established(&self) -> bool pub fn touch<C: ProxyCoreDecode>(&mut self, fx: &mut Effects<'_, C>) pub fn enter<C: ProxyCoreDecode>(&mut self, phase: Phase, fx: &mut Effects<'_, C>) pub fn expired<C: ProxyCoreDecode>(&mut self, fx: &mut Effects<'_, C>) -> Expired}客户端一言不发时,运行时不会交付任何事件,因此 app 还在运行时之外监视握手:
async fn drive<const BUF: usize, Core, S>( stream: S, core: Core, connector: AppConnector, handshake: Arc<OwnedSemaphorePermit>,) -> io::Result<()>where S: AsyncRead + AsyncWrite + Unpin, Core: ProxyCoreDecode<Target = Flow, Error = io::Error, TransportAddr = ()> + Established,
pub trait Established { fn is_established(&self) -> bool;}其他涉及限制的入口
Section titled “其他涉及限制的入口”pub fn set_keepalive(stream: &TcpStream)
// environment/src/dial/tcp.rsimpl TcpDialer { pub fn with_connect_timeout(mut self, timeout: Duration) -> Self pub async fn connect(&self, addr: SocketAddr) -> io::Result<TcpStream> pub async fn connect_any(&self, addrs: &[SocketAddr]) -> io::Result<TcpStream>}
// environment/src/dial/socket.rsimpl SocketOptions { pub fn with_tcp_keepalive(mut self, idle: Duration) -> Self}
// protocols/src/transports/ws/endpoint.rspub(crate) fn ws_config() -> tokio_tungstenite::tungstenite::protocol::WebSocketConfig
// protocols/src/transports/grpc/liveness.rsimpl Liveness { pub(crate) fn new(ping_pong: Option<PingPong>) -> Self pub(crate) fn note_progress(&mut self) pub(crate) async fn watch(&mut self, idle: bool)}
// protocols/src/dns/mod.rsimpl Resolver { pub fn with_ttl_bounds(self, min: Duration, max: Duration) -> Self pub async fn resolve(&self, host: &str) -> io::Result<Vec<IpAddr>>}
// protocols/src/hysteria/slot.rs (ConnSlot) and protocols/src/wireguard/slot.rs (DeviceSlot)pub fn died_young(&self) -> boolpub fn note_failure(&mut self)
// app/src/balancer.rsimpl Balancer { pub fn spawn_probe( self: &Arc<Self>, token: CancellationToken, interval: Duration, timeout: Duration, resolver: Resolver, )}下图跟随一条 TCP 连接穿过 etemenanki-app,并标出每组限制作用的位置。Hysteria 2 和 TUN 入站自己持有 socket,并自行做准入,下文各自的小节会介绍。
flowchart TB accept["accept 循环:ACCEPT_ERROR_BACKOFF"] admit["先取 live 许可,再取 handshake 许可"] transport["InboundTransport::accept:keepalive,TRANSPORT_HANDSHAKE_TIMEOUT"] carrier["WS 与 gRPC:消息上限,空闲与 PING 截止时间"] runtime["ProxyServerRuntime:3 x BUF_SIZE,FrameTooLarge"] timing["协议核心 Timing:握手、嗅探、空闲"] watchdog["drive:建立前每一步 HANDSHAKE_TIMEOUT"] mux["mux.cool Demux:MAX_SESSIONS,MAX_DATA_LEN"] connector["AppConnector"] tcp["freedom:每个地址 DEFAULT_CONNECT_TIMEOUT"] fanout["UDP 流:FanOutLink,MAX_SUBS"] client["代理出站:ProxyClientRuntime,2 x BUF"] dns["Resolver:QUERY_TIMEOUT,CACHE_CAPACITY"] accept --> admit --> transport --> carrier --> runtime runtime --> timing watchdog --> runtime runtime --> mux runtime --> connector mux --> connector connector --> tcp connector --> fanout connector --> client tcp --> dns
每个协议核心恰好有一个截止时间,其含义取决于所处阶段。Timing::touch 在每个携带字节的事件开头运行,Timing::enter 切换到新阶段并设置该阶段的截止时间:
stateDiagram-v2 [*] --> Handshake: 运行时已构建,尚未设置截止时间 Handshake --> Handshake: 首个字节事件设置 HANDSHAKE_TIMEOUT Handshake --> Sniff: 请求已解析,目标为 IP,嗅探开启 Handshake --> Relay: 请求已解析,流已打开 Sniff --> Relay: 找到域名、预算用尽或 SNIFF_TIMEOUT Relay --> Relay: 每个字节事件重新设置 RELAY_IDLE_TIMEOUT Relay --> Closing: Expired Idle,Timing 推送 Finish Relay --> [*]: 两个方向都已关闭,协议核心推送 Finish Closing --> [*]: 运行时结束 Handshake --> [*]: Expired Handshake,协议核心返回错误
没有哪个协议核心会自己进入 Phase::Closing。只有 Timing::expired 会把一条正在中继的连接移到这个阶段,并在同一步中推送 Effect::Finish。单流中继的两个方向都正常关闭时,Passthrough 在 Timing 仍处于 Phase::Relay 时推送 Finish。
两道握手防线互为补充:
| 客户端行为 | 由谁终止 | 错误文本 |
|---|---|---|
| 什么都不发送 | drive 把每次 runtime.next() 包进 tokio::time::timeout(HANDSHAKE_TIMEOUT, …),直到协议核心报告 is_established() |
inbound handshake timed out after 10s |
| 发送首个字节,然后一点点发送其余部分 | Timing 在首个字节事件时设置一次 HANDSHAKE_TIMEOUT,在 Phase::Handshake 中不再刷新,因此它限制的是整个请求,而不是字节之间的间隔 |
client did not complete its request in time |
相比之下,空闲截止时间在 Relay 和 Closing 中每个字节事件都会重新设置。这使它成为空闲限制,而不是生命周期限制。
按层划分的限制
Section titled “按层划分的限制”数值都是精确值。源码写作 n * 1024 的地方,这里以 KiB 或 MiB 给出。
运行时与协议核心
Section titled “运行时与协议核心”| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
BUF_SIZE(const 泛型) |
因协议核心而异,见 服务端协议核心 | concepts/src/runtime.rs 中的 ProxyServerRuntime |
决定堆上数组 up(一个 ReadBuffer)、staging(一个 WriteBuffer)和 scratch 的大小。 |
ProxyCoreDecode::STAGING_RESERVE |
因协议核心而异 | concepts/src/core.rs |
只有当 staging.room() 不小于预留量时,poll_transport_read 才读取传输层。流式出站只在剩余空间达到 STAGING_RESERVE + 1 字节时才读取,且最多读入 scratch 的 room - STAGING_RESERVE 字节。数据报出站只在剩余空间达到 STAGING_RESERVE + datagram_limit() 时才读取。 |
ProxyCoreDecode::MAX_DATAGRAM |
默认 4096;部分协议核心会调高 | concepts/src/core.rs |
数据报出站读入 scratch[..datagram_limit()]。数据报传输层读入 up.free()[..MAX_DATAGRAM.min(BUF_SIZE)]。 |
RuntimeError::FrameTooLarge |
未解析区域填满 BUF_SIZE 时 |
concepts/src/runtime.rs 的 poll_transport_read 中 |
协议核心仍需要更多数据时 ReadBuffer::is_saturated()(start == 0 && end == N)为真。错误文本:protocol frame exceeds the read buffer。 |
客户端 down 饱和 |
尚未解开的帧填满客户端的 BUF_SIZE 时 |
concepts/src/client.rs |
对客户端运行时的 down 缓冲区做同样的 is_saturated() 检查。错误文本:upstream frame larger than the client runtime's buffer。在发送方向上,如果一个数据包加上预留量连空的 staging 缓冲区都放不下,make_room 会以 frame larger than the client runtime's buffer 失败。 |
WORK_BUDGET |
64 个工作单元 | concepts/src/runtime.rs |
静默运行时的 Future::poll 最多调用 poll_once 64 次,然后唤醒自己并返回 Pending,这样繁忙的连接无法独占 worker。进度模式(showing_progress)是一个 Stream,每完成一个单元就让出一次。在 app 中,Hysteria 2 的流和数据报运行时以及 TUN 的 UDP 关联以静默 future 的方式 await;drive 和 TUN 入站的 serve_stream 使用进度模式。 |
INLINE_EFFECTS |
4 | concepts/src/core.rs |
EffectList(一个 SmallVec)的内联容量。更长的 effect 列表会溢出到堆上。按 Open + Forward + SetDeadline 确定大小。 |
HANDSHAKE_TIMEOUT |
10 s | protocols/src/core/mod.rs |
每个协议核心中的 Timing。app 的 drive、TUN 入站的 serve_stream 和 SOCKS 驱动也用同一个常量实现各自的看门狗。 |
RELAY_IDLE_TIMEOUT |
300 s | protocols/src/core/mod.rs |
在 Relay 和 Closing 中,Timing::touch 在每个字节事件时重新设置它。到期时 Timing::expired 推送 Effect::Finish。SOCKS 驱动复用了它,见 协议帧上限与驱动。 |
SNIFF_TIMEOUT |
300 ms | protocols/src/sniff/mod.rs |
Timing::enter(Phase::Sniff, …)。到期时协议核心用已收集的内容打开流。 |
SNIFF_LIMIT |
4 KiB,跨帧累计 | protocols/src/sniff/mod.rs |
一旦持有 4 KiB,Collector::push 返回 Verdict::Exhausted;Collector::remaining 永远不会给出更多。 |
MAX_DOMAIN_LEN |
253 | protocols/src/sniff/mod.rs |
plausible_domain 拒绝更长的嗅探主机名。 |
HARNESS_STAGING |
64 KiB | protocols/src/core/harness.rs |
CoreHarness 每次调用为协议核心提供的 staging 空间。该 harness 在测试中手动驱动协议核心,不做任何 I/O;没有生产路径使用它。 |
BidirectionalConnection 的 BUF_SIZE |
默认每个方向 8192 | concepts/src/relay.rs |
每个方向一个 Box<[u8; BUF_SIZE]>。SOCKS 驱动用 RELAY_BUF 实例化它。 |
每个已打开的出站还要占用一个 Arc<KeyWaker>(concepts/src/wake.rs)、拨号期间 connector 的 future,以及一个 BTreeMap 条目。运行时本身不限制出站数量。做多路复用的协议核心会限制它,见 Mux。
传输层与拨号
Section titled “传输层与拨号”| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
TRANSPORT_HANDSHAKE_TIMEOUT |
10 s | protocols/src/transports/accept.rs |
within() 包住 InboundTransport::accept 的一个步骤:对 tls 是 TLS accept;对 ws 和 grpc,则是可选的 TLS accept 加上 WebSocket 升级或 HTTP/2 握手,共用一个 10 s 预算。错误文本:tls handshake timed out、websocket handshake timed out、grpc handshake timed out;基于 TLS 的 WebSocket 或 gRPC 传输即使慢在 TLS,报告的也是它自己的名字。serve_socket 在 debug 级别把失败记录为 inbound transport failed: <error>。纯 TCP 和 Unix socket 没有传输层步骤。 |
TCP_KEEPALIVE_IDLE |
120 s | protocols/src/transports/keepalive.rs |
set_keepalive,应用于 InboundTransport::accept 收到的每个 TCP socket,以及 TransportConnector::dial 打开的每个 socket。尽力而为:平台拒绝该选项时,在 debug 级别记录 could not enable TCP keepalive 并保留连接。 |
TCP_KEEPALIVE_INTERVAL |
30 s | 同上 | 同上 |
TCP_KEEPALIVE_RETRIES |
3 | 同上 | 同上 |
DEFAULT_CONNECT_TIMEOUT |
每个地址 10 s | environment/src/dial/tcp.rs |
TcpDialer::connect 把 socket.connect 包进 tokio::time::timeout:connect to <addr> timed out。connect_any 逐个尝试地址(不是 Happy Eyeballs),因此最坏情况是 10 s 乘以地址数;全部失败时返回 failed to connect to any address (…),附带每个地址的错误。freedom 出站和 TransportConnector 使用它。with_connect_timeout 可覆盖该值,但内核和 app 中都没有调用它。 |
SocketOptions::tcp_keepalive |
默认 None |
environment/src/dial/socket.rs |
用 with_tcp_keepalive 设置后,TcpDialer::socket 会设置 keepalive 空闲时间。内核和 app 中都没有设置它。app 的 freedom 出站用 SocketOptions::default() 构建拨号器,也不调用 set_keepalive,因此它的 socket 沿用平台的 keepalive 默认值。 |
MAX_EARLY_DATA |
16 KiB | protocols/src/transports/ws/endpoint.rs |
入站:decode_early_data_header 拒绝解码后更大的 Sec-WebSocket-Protocol 载荷,升级请求以 413 Payload Too Large 应答。出站:?ed=N 限制被钳制为 N.min(MAX_EARLY_DATA)。 |
MAX_WS_MESSAGE_LEN |
1 MiB | protocols/src/transports/ws/endpoint.rs |
ws_config() 同时设置 max_message_size 和 max_frame_size,取代 tungstenite 的默认值 64 MiB 和 16 MiB,作用于每个会话,无论是接受的还是拨出的。 |
WS_KEEPALIVE_INTERVAL |
60 s | protocols/src/transports/ws/stream.rs |
该计时器触发时,WsStream 的读端排入一个 Ping。每收到一帧、每次写入,touch() 都会重置它。 |
WS_IDLE_TIMEOUT |
300 s | protocols/src/transports/ws/stream.rs |
连续 300 s 没有收到帧也没有写入时,读端报告流结束。任何帧都算数,包括 Pong,正因如此 Ping 才能充当存活探测。 |
H2_INITIAL_STREAM_WINDOW_SIZE |
4 MiB | protocols/src/transports/grpc/settings.rs |
configured_server_builder 和 configured_client_builder(HTTP/2 流量控制)。 |
H2_INITIAL_CONNECTION_WINDOW_SIZE |
16 MiB | 同上 | 同上 |
H2_MAX_FRAME_SIZE |
256 KiB | 同上 | 同上 |
H2_MAX_CONCURRENT_STREAMS |
256 | 同上 | 仅服务端 builder:每个 HTTP/2 连接最多 256 条 gRPC 隧道流。 |
MAX_GRPC_MESSAGE_LEN |
1 MiB | 同上 | grpc/framing.rs 中的 Hunk 解码器在缓冲消息体之前先检查声明的长度:grpc message length exceeds the accepted maximum。 |
H2_IDLE_TIMEOUT |
300 s | 同上 | 服务端连接连续 300 s 没有承载任何流时,Liveness 结束它。serve_h2 在每接受一条流时,以及有流打开期间流数量每次变化时调用 note_progress,从而重新开始计时。 |
H2_KEEPALIVE_INTERVAL |
60 s | 同上 | Liveness 在服务端连接上每 60 s 发送一个 PING。 |
H2_KEEPALIVE_TIMEOUT |
20 s | 同上 | PING 在 20 s 内未得到应答时,结束该服务端连接。 |
准入限制决定一条新连接、流或流是否会被接纳。除 QUIC 流额度(DEFAULT_MAX_INCOMING_STREAMS)外,它们都不排队:每一项都用 try_acquire_owned 或 try_send,拒绝是立即发生的。
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
MAX_LIVE_CONNECTIONS_PER_INBOUND |
65,536 | app/src/serve.rs |
每个流式入站一个 Semaphore。run_stream_inbound 为每个已接受的 socket 取一个许可,serve_connection 持有它(作为 _session),直到该 socket 及其产生的每条流都结束。达到上限时丢弃该 socket,循环在 debug 级别记录 dropping inbound connection; live connection limit reached。 |
MAX_HANDSHAKES_PER_INBOUND |
2048 | app/src/serve.rs |
每个流式入站的第二个 Semaphore,在 live 许可之后获取。协议核心报告 is_established() 后,drive 释放其握手许可(handshake.take())。达到上限时:在 debug 级别记录 dropping inbound connection; handshake limit reached。 |
ACCEPT_ERROR_BACKOFF |
100 ms | app/src/serve.rs |
出现 ConnectionAborted 和 Interrupted 以外的 accept 错误(例如 EMFILE)后,should_backoff_accept_error 为真,循环在 warn 级别记录 accept error, backing off 100ms,并休眠 100 ms 或直到被取消。 |
DEFAULT_MAX_CONNECTIONS |
4096 | protocols/src/hysteria/server/config.rs |
Hysteria 2 监听器:每个 QUIC 连接一个许可,在握手完成前获取,在连接存续期间一直持有。达到上限时 incoming.refuse() 立即告知客户端。配置键 max_connections。 |
DEFAULT_MAX_CIRCUITS |
65,536 | protocols/src/hysteria/server/config.rs |
ServerConfig::circuit_permits,由监听器上的所有连接共享。代理流在其运行时被 spawn 之前获取一个许可,并把它移入 task。UDP 会话在打开时获取一个许可,并保存在其 Session 中。达到上限时,流以 H3_REQUEST_REJECTED 重置,数据报被丢弃。配置键 max_circuits。 |
DEFAULT_MAX_INCOMING_STREAMS |
1024 | protocols/src/hysteria/server/endpoint.rs |
QUIC 传输配置中的 max_concurrent_bidi_streams。QUIC 会扣住流额度,所以客户端的 open_bi 是等待而不是被拒绝。 |
MAX_CLASSIFYING_STREAMS |
64 | protocols/src/hysteria/server/inbound.rs |
单个连接中 HTTP/3 层与流分类器之间两个 mpsc 通道的容量。通往分类器的通道已满时,Hy2H3Conn::poll_accept_bidi(protocols/src/hysteria/server/shim.rs)以 H3_EXCESSIVE_LOAD 重置新流,并在 debug 级别记录 hysteria2: resetting a stream; too many are awaiting classification。 |
DEFAULT_MAX_FLOWS |
65,536 | protocols/src/tun/config.rs |
TUN:整个设备共用一个 Semaphore。每条 TCP 流和每个 UDP 关联(每个客户端源地址一个)各取一个许可。达到上限时丢弃该流:在 debug 级别记录 tun: dropping a flow; the flow limit is reached。配置键 max_flows。 |
FLOW_QUEUE |
16 | protocols/src/tun/udp.rs |
把同一客户端源的新流交给其关联的通道的容量。通道已满时 try_send 失败,新流被丢弃;客户端会重传。 |
mux.cool 解复用器 Demux 由 Trojan、VLESS 和 VMess 协议核心共用。
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
MAX_SESSIONS |
每个承载连接 256 条子流 | protocols/src/mux/demux.rs |
已有 256 个会话打开,或该 id 已在使用中时,Demux 以 End 应答 New 帧并丢弃其载荷。承载连接保持不断。 |
MAX_META_LEN |
512 | protocols/src/mux/frame.rs |
帧解析器拒绝更长的元数据块:mux: metadata length … exceeds 512。 |
MAX_DATA_LEN |
8 KiB | protocols/src/mux/frame.rs |
帧解析器拒绝更长的数据块:mux: data length … exceeds 8192。在下行方向,Demux::on_outbound 把一次读取拆成每个最多 8 KiB 的 Keep 帧,Demux::on_datagram 丢弃长于 8 KiB 的数据包。 |
FRAME_OVERHEAD_MAX |
268 | protocols/src/mux/frame.rs |
2 + MIN_META_LEN + 1 + AddressCodec::MAX_LEN + 2,其中 MIN_META_LEN = 4:最大的 Keep 头部。 |
downlink_overhead(16 * 1024) |
536 | protocols/src/mux/demux.rs |
两个 Keep 头部。是 TrojanCore 和 VlessCore staging 预留量的一部分。 |
UDP fan-out
Section titled “UDP fan-out”app 不会把一条 UDP 流作为整体来拨号。AppConnector::connect 返回一个 FanOutLink,它按每个数据报各自的目的地路由,并为每个被选中的出站保留一个子 link。
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
MAX_SUBS |
每个关联 64 个子 link | app/src/outbound/udp_fanout.rs |
FanOutLink::poll_opening 放入新的子 link;若此时表中超过 64 个,就移除最前面的条目。每次发送都会把对应子 link 移到末尾,因此被淘汰的是最久没有发送过的那个。丢弃它会关闭该出站的 socket。 |
打开失败的子 link 会在 debug 级别记录(udp fan-out: opening an outbound failed),触发它的那个数据包不会被送达。
Hysteria 2
Section titled “Hysteria 2”服务端:
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
STREAM_RECEIVE_WINDOW |
8 MiB | protocols/src/hysteria/server/endpoint.rs |
QUIC 每条流的接收窗口。 |
CONNECTION_RECEIVE_WINDOW |
20 MiB(STREAM_RECEIVE_WINDOW / 2 * 5) |
同上 | QUIC 每个连接的接收窗口。 |
MAX_IDLE_TIMEOUT |
30 s | 同上 | QUIC max_idle_timeout:quinn 关闭静默 30 s 的连接。 |
CLASSIFY_TIMEOUT |
10 s | protocols/src/hysteria/server/inbound.rs |
流必须在 10 s 内表明其帧类型,否则以 H3_REQUEST_CANCELLED 重置。 |
DRAIN_TIMEOUT |
3 s | 同上 | Hy2Inbound::shutdown 以 CLOSE_CODE 关闭 endpoint,然后最多等待 3 s 让 endpoint.wait_idle() 完成。 |
RELEASE_TIMEOUT、RELEASE_POLL |
3 s、20 ms | 同上 | 随后 shutdown 每 20 ms 尝试绑定一次该端口,最多持续 3 s,以便下一个 generation 能绑定它。超时会在 warn 级别记录 hysteria2: <addr> did not come free within 3s。 |
CLOSE_CODE |
0x100 |
同上 | 拆除时发给每个客户端的 QUIC 应用错误码。 |
MAX_SESSIONS |
每个连接 256 个 UDP 会话 | protocols/src/hysteria/server/datagrams.rs |
已有 256 个会话打开时,Hy2UdpCore 丢弃每个指向新会话 id 的数据包(在 trace 级别记录)。 |
SWEEP_INTERVAL |
1 s | 同上 | 该协议核心的截止时间。每次到期都推进其 tick 时钟,并关闭已静默 udp_idle_timeout 整秒(至少 1 秒)的会话。 |
udp_idle_timeout |
默认 60 s,接受 2 到 600 s | app/src/inbound/mod.rs |
build_hysteria2_inbound 拒绝超出范围的值,并在 udp 关闭时拒绝该键。 |
协议与客户端:
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
MAX_ADDRESS_LENGTH |
2048 | protocols/src/hysteria/protocol.rs |
TCPRequest 和 UDPMessage 地址的编码与解析。 |
MAX_MESSAGE_LENGTH |
2048 | 同上 | 可编码或解析的最长 TCPResponse 消息。 |
MAX_MESSAGE_KEPT |
128 | 同上 | 服务器提供的消息经过净化后,最多有多少个字符保留到客户端的 io::Error 中。 |
MAX_PADDING_LENGTH |
4096 | 同上 | 任何帧上接受的最长填充。 |
MAX_UDP_SIZE |
4096 | 同上 | Defragger 拒绝重组超过 4096 字节的数据。也是 Hy2UdpCore::MAX_DATAGRAM。 |
MAX_DATAGRAM_FRAME_SIZE |
1200 | 同上 | 连接未报告 max_datagram_size() 时,服务端的数据报 task 把它作为对端的数据报大小传给 Hy2UdpCore;更长的回复会被分片。 |
MIN_PSK_LEN |
4 字节 | protocols/src/hysteria/obfs.rs |
Salamander 密钥的最短长度。app 在两端都拒绝更短的 obfs_password:obfs_password must be at least 4 bytes for salamander。 |
CONNECT_TIMEOUT |
每个地址 10 s | protocols/src/hysteria/connection.rs |
Hy2Conn::connect 为每次地址尝试设定上限:socket 绑定、QUIC 握手和 /auth 往返。服务器名在尝试之前解析一次,不在此超时之内。超时的尝试记为 <addr>: timed out,所有地址都失败时错误为 hysteria2: no address answered (…)。 |
OPEN_STREAM_TIMEOUT |
5 s | 同上 | 打开代理流时限制 open_bi:hysteria2: timed out opening a proxy stream。 |
DEFAULT_MAX_CONCURRENT_STREAMS |
102,400 | protocols/src/hysteria/config.rs |
客户端的 streams 信号量。在打开流之前用 try_acquire_owned 获取许可,中继期间一直持有:hysteria2: connection is at its concurrent-stream limit。该默认值远高于默认服务端的 1024 条流,因此实际上服务端的流额度先生效,open_bi 会等待,最长 OPEN_STREAM_TIMEOUT。配置键 max_concurrent_streams;0 被拒绝。 |
MAX_UDP_SESSIONS |
256 | protocols/src/hysteria/connection.rs |
hysteria2: connection is at its UDP association limit。 |
UDP_SESSION_BACKLOG |
256 个数据报 | 同上 | 每个关联接收通道的容量。通道已满时 try_send 失败,数据报被丢弃,这样慢速消费者永远不会卡住连接的读取端。 |
KEEP_ALIVE |
10 s | 同上 | 客户端的 QUIC keep_alive_interval。客户端的窗口和 MAX_IDLE_TIMEOUT 与服务端一致。 |
RECONNECT_BACKOFF_BASE、RECONNECT_BACKOFF_MAX |
1 s、30 s | protocols/src/hysteria/slot.rs |
ConnSlot::note_failure 把 retry_at 设为 BASE * 2^min(failures, 5),上限为 MAX:2 s、4 s、8 s、16 s,然后是 30 s。 |
MIN_HEALTHY_LIFETIME |
10 s | 同上 | 启动后 10 s 内断开的连接算作一次失败(died_young),因此面对先认证再关闭的服务器时,退避会不断累积。 |
WireGuard
Section titled “WireGuard”| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
DEFAULT_MTU |
1420 | protocols/src/wireguard/config.rs |
InMemoryDevice 把它报告为 smoltcp 的 max_transmission_unit。配置键 mtu。 |
CHANNEL_CAP |
256 个条目 | protocols/src/wireguard/device.rs |
设备命令通道的容量,以及每条隧道流与应用侧之间两个 mpsc 通道的容量。这些通道按条目计数,而不是按字节:一次 WgStream 写入就是一个条目,大小等于调用方的缓冲区。驱动为每条连接最多持有一个上行条目(Uplink),并且只在没有持有条目时才读取该连接的通道,因此一条连接在上行方向最多能排队的是它 64 KiB 的 TCP_BUFFER 发送环,加上 256 个通道条目,再加 1 个被持有的条目;超过这个量,写入方就要等待。CHANNEL_CAP 也是驱动每一轮最多交给一条连接的 socket 的条目数,因此没有哪个生产者能独占驱动。在下行方向,驱动只在该连接的通道还有空间时才读取 socket。 |
TCP_BUFFER |
64 KiB | 同上 | 每个 smoltcp TCP 环形缓冲区(每个方向一个)的大小,也是每个 UDP 数据包缓冲区(每个方向一个,带 64 个元数据槽)的载荷存储大小。遇到发送环已满的 UDP 数据报会被暂存,直到下一次 poll 把发送环清空。大于整个 64 KiB 发送环的数据报永远发不出去,因此驱动会丢弃它(在 trace 级别记录),而不是反复重试,后续数据报照常流动。 |
SCRATCH |
64 KiB | 同上 | 驱动的封装 scratch 缓冲区及其 UDP 接收缓冲区,每个设备各一份。 |
TIMER_TICK |
250 ms | 同上 | 驱动 Tunn::update_timers 的 tokio::time::interval。 |
EPHEMERAL_BASE |
49152 | 同上 | 隧道侧的第一个源端口;端口从 65535 回绕到 49152。 |
| smoltcp 轮询延迟 | 有待处理的重新轮询时为 2 ms,否则为 smoltcp 自己给出的延迟,再否则为 3600 s | 同一文件中的 compute_sleep |
内联字面量,不是具名常量。 |
TCP_CONNECT_ATTEMPT_TIMEOUT |
每个地址 10 s | protocols/src/wireguard/slot.rs |
connect_tcp_any 为每次隧道内 TCP 连接设定上限。 |
REBUILD_BACKOFF_BASE、REBUILD_BACKOFF_MAX |
1 s、30 s | 同上 | DeviceSlot::note_failure,算法与 Hysteria slot 相同。slot 处于退避期间,acquire_device 以 wireguard: tunnel is down, waiting before the next attempt 失败,不构建隧道。 |
MIN_HEALTHY_LIFETIME |
10 s | 同上 | 10 s 内死亡的设备算作一次启动失败,因为 WgDevice::start 不做握手,即使对端不可达也会成功。 |
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
DEFAULT_MTU |
1500 | protocols/src/tun/config.rs |
传给设备和 IpStackConfig::mtu。app/src/inbound/tun.rs 拒绝低于 1280 的 mtu 配置:tun mtu must be at least 1280。 |
DEFAULT_UDP_IDLE_TIMEOUT |
60 s | 同上 | IpStackConfig::udp_timeout:协议栈结束静默的(源,目的)UDP 流。配置键 udp_idle_timeout。 |
DEFAULT_MAX_FLOWS |
65,536 | 同上 | 见 准入。 |
FLOW_QUEUE |
16 | protocols/src/tun/udp.rs |
见 准入。 |
RELEASE_TIMEOUT、RELEASE_POLL |
3 s、20 ms | protocols/src/tun/inbound.rs |
TunInbound::shutdown 每 20 ms 轮询一次,最多持续 3 s,直到设备描述符和每条被跟踪的 TCP 流都被释放。超时会记录一条 warn。 |
TUN UDP 有两个计时器。协议栈的 udp_timeout 结束每条(源,目的)流;关联的 TunUdpCore 运行 Timing,因此整个关联静默达到 RELAY_IDLE_TIMEOUT 时就会结束。
TUN 入站中的 TCP 流要等到客户端发来第一批字节后才拨号出站。因此 serve_stream 在 PassthroughCore 建立之前,把每个运行时步骤都包进 HANDSHAKE_TIMEOUT,服务端先发言的协议会在 10 s 后以 tun: the client never spoke 结束。
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
CACHE_CAPACITY |
8192 个名称 | protocols/src/dns/mod.rs |
moka::future::Cache::max_capacity。缓存键是客户端可以任意选择的名称,因此缓存必须有界。 |
SYSTEM_TTL |
60 s | 同上 | Backend::System 应答的 TTL,因为 getaddrinfo 不报告 TTL。 |
MIN_TTL、MAX_TTL |
5 s、3600 s | 同上 | resolve 在缓存应答之前把每个 TTL 钳制到该范围内。MAX_TTL 也是 moka 整个缓存的 time_to_live。with_ttl_bounds 可覆盖该范围;app 没有调用它。 |
QUERY_TIMEOUT |
5 s | 同上 | 包住 Udp、Tls 和 Https 后端的每次查询:dns: query timed out。A 和 AAAA 查询并发进行,各有自己的超时。Backend::System 走 tokio::net::lookup_host,不受此超时包裹。 |
UDP_BUF |
512 字节 | 同上 | 纯 UDP 后端的接收缓冲区。不请求 EDNS。 |
MAX_POINTER_HOPS |
16 | protocols/src/dns/message.rs |
skip_name 在放弃之前最多跳过一个名称的 16 个标签(Truncated("dns name pointer chain"))。它从不跟随压缩指针(指针会就地结束名称),所以指针环不会导致死循环。 |
MAX_NAME_LEN |
255 | 同上 | encode_query 接受的最长名称:dns: name too long (<n> bytes)。 |
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
DEFAULT_PROBE_INTERVAL |
30 s | app/src/balancer.rs |
spawn_probe 为每个成员运行一个 task。它先立即探测一次,此后每次探测结束都休眠这么长时间,因此周期等于探测耗时加上该间隔。配置键 probe_interval(秒)。 |
DEFAULT_PROBE_TIMEOUT |
5 s | 同上 | 一次探测会解析成员的上游,并依次尝试对每个地址做 TCP 连接,全部在同一个 tokio::time::timeout 之内。配置键 probe_timeout(秒)。 |
成员初始为健康状态。状态变化在 info 级别记录为 balancer member <tag> is now up 或 down。所有成员都不可用时,Balancer::select 回退到第一个成员,而不是让该流失败。两个键都接受 0:probe_timeout = 0 让探测没有时间建立连接,于是成员被标记为不可用,流量回退到第一个成员;probe_interval = 0 则让探测一次接一次地连续进行。
协议帧上限与驱动
Section titled “协议帧上限与驱动”这些常量限制解析器接受什么、编码器产出什么,或者协议驱动缓冲多少数据。解析器上限会让帧在解析处失败。没有解析器上限的帧只受它必须放入的缓冲区约束,放不下时连接以 FrameTooLarge 结束。
| 常量 | 取值 | 定义位置 | 含义 |
|---|---|---|---|
MAX_HEAD |
64 KiB | protocols/src/http/protocol.rs |
最长的 HTTP 请求头部,也是 HttpCore::BUF_SIZE。未解析字节达到该值却仍没有完整头部时,HttpCore 以 http head exceeds maximum size 失败。HttpConnect::reply 对代理的响应头部检查同样的上界(http: proxy response head exceeds maximum size),但在 app 中,它的客户端运行时只有 HTTP_BUF(16 KiB),会先饱和。 |
MAX_HEADERS |
128 | 同上 | 每个头部可用的 httparse header 槽位数。 |
HttpConnect::REQUEST_MAX |
1024 | protocols/src/http/codec.rs |
HTTP 客户端 stage 的 CONNECT 请求的上界,也是它的 STAGING_RESERVE。 |
AddressCodec::MAX_LEN |
259 | protocols/src/helpers/address.rs |
1 + 1 + 255 + 2:类型、长度、域名、端口。是每个包含地址的预留量的一部分。 |
RELAY_BUF |
16 KiB | protocols/src/socks/server.rs |
SOCKS CONNECT 中继每个方向的缓冲区(BidirectionalConnection<A, B, RELAY_BUF>)。每隔 RELAY_IDLE_TIMEOUT,relay_with_idle_guard 把已中继的字节数与上次检查时比较,没有任何数据流动就以错误 socks: relay idle 结束中继。因此空闲的中继会在最后一个字节之后 300 s 到 600 s 之间结束。 |
HUB_BUF |
64 KiB | 同上 | SOCKS UDP ASSOCIATE 中继的两个数据包缓冲区,每个的大小。关联每在任一方向转发一个数据包,就重置它的 RELAY_IDLE_TIMEOUT 计时器,计时器触发时关联结束。ExpectedSender::admits 在解析数据包之前检查每个数据报的发送方:来自客户端以外 IP 的数据报,或者在客户端端口被固定之后来自其他端口的数据报,都会被丢弃且不重置计时器,因此来自其他地址的流量无法让关联一直保持打开。见关联的客户端。 |
RECV_BUF |
64 KiB | protocols/src/socks/udp_link.rs |
SocksUdpLink(SOCKS 客户端的 UDP 关联)的接收缓冲区。每个数据报都会读入其中,发送方不是中继的数据报(用 endpoint(protocols/src/socks/protocol.rs)比较)会在解析之前被丢弃。 |
MAX_LENGTH |
8192 | protocols/src/trojan/protocol.rs |
最大的 Trojan UDP 数据包载荷,也是 TrojanCore::MAX_DATAGRAM。 |
MAX_PAYLOAD(VMess) |
1966 | protocols/src/vmess/framing.rs |
2048 - TAG_SIZE - 2 - 64:VMessCore 和 VMess codec 在一个被 seal 的 body chunk 中放入的最多明文,保证加上填充的 chunk 不超过 2 KiB。 |
MAX_PADDING、CHUNK_OVERHEAD_MAX |
64、82 | 同上 | chunk 上最长的全局填充,以及一个被 seal 的 chunk 在明文之外最多增加的字节数(2 + TAG_SIZE + MAX_PADDING)。 |
AUTHID_WINDOW_SECS |
120 s | protocols/src/vmess/accounts.rs |
auth id 的时间戳与服务器时钟相差在 120 s 以内(无论哪个方向)时才被接受。 |
REPLAY_TTL |
240 s | 同上 | 2 * AUTHID_WINDOW_SECS:已见过的 auth id 被记住多久,分布在 REPLAY_SHARDS(64)个分片中。 |
MAX_PAYLOAD(Shadowsocks) |
0x3FFF |
protocols/src/ss_legacy/aead.rs |
本端 seal 的最大 AEAD chunk 载荷。CHUNK_OVERHEAD 为 34(2 + 16 + 16)。chunk 解码器不会拿收到的长度与它比较;收到的 chunk 只需放得进 20 KiB 的缓冲区(ShadowsocksCore::BUF_SIZE、SS_BUF),否则连接以 FrameTooLarge 结束。 |
MAX_PADDING_LENGTH |
900 | protocols/src/ss_2022/protocol.rs |
初始载荷短于 900 字节时,Shadowsocks 2022 客户端给请求加上 1 到 900 个随机填充字节。 |
MAX_PACKET_SIZE |
0xFFFF |
protocols/src/ss_2022/crypto.rs |
最大的 Shadowsocks 2022 record 载荷。服务端协议核心和客户端 codec 把要 seal 的数据拆成每个最多这么大的 record。收到的 record 就地打开,因此必须放得进 32 KiB 的缓冲区,见 服务端协议核心。 |
| 常量 | 取值 | 定义位置 | 执行机制 |
|---|---|---|---|
| 重载防抖 | 200 ms | app/src/main.rs(spawn_watcher 中的内联字面量) |
配置监视器先取空排队的文件事件,休眠 200 ms 后再取空一次,然后只调用一次 reload()。 |
HTTP_BUF、SOCKS_BUF、TROJAN_BUF、VLESS_BUF |
16 KiB | app/src/outbound/mod.rs |
客户端运行时缓冲区大小,见 客户端 codec。 |
VMESS_BUF、SS2022_BUF |
32 KiB | 同上 | 同上 |
SS_BUF |
20 KiB | 同上 | 同上 |
可配置的限制
Section titled “可配置的限制”只有下面这些限制从配置文件读取。本页其他所有内容都是编译进去的。
| 配置键 | app/src/config.rs 中的结构体 |
缺省时的默认值 | app 中的校验 |
|---|---|---|---|
max_connections |
Hysteria2InboundSettings |
DEFAULT_MAX_CONNECTIONS(4096) |
拒绝 0:max_connections must be at least 1 |
max_circuits |
Hysteria2InboundSettings |
DEFAULT_MAX_CIRCUITS(65,536) |
拒绝 0:max_circuits must be at least 1 |
udp_idle_timeout |
Hysteria2InboundSettings |
60 s | 2 到 600 s:udp_idle_timeout must be between 2 and 600 seconds。udp 关闭时拒绝:udp_idle_timeout is set but udp is not enabled |
max_concurrent_streams |
Hysteria2OutboundSettings |
DEFAULT_MAX_CONCURRENT_STREAMS(102,400) |
拒绝 0:max_concurrent_streams must be at least 1。没有上限。 |
mtu |
TunInboundSettings |
DEFAULT_MTU(1500) |
至少 1280 |
udp_idle_timeout |
TunInboundSettings |
DEFAULT_UDP_IDLE_TIMEOUT(60 s) |
无 |
max_flows |
TunInboundSettings |
DEFAULT_MAX_FLOWS(65,536) |
无。接受 0,此时不接纳任何流。 |
obfs_password |
Hysteria2InboundSettings、Hysteria2OutboundSettings |
无 | obfs = "salamander" 时至少 MIN_PSK_LEN(4)字节:obfs_password must be at least 4 bytes for salamander |
mtu |
WireguardOutboundSettings |
DEFAULT_MTU(1420) |
无 |
keepalive |
WireguardOutboundSettings |
无(不做持久 keepalive) | 一个 u16 秒数,作为持久 keepalive 间隔传给 boringtun 的 Tunn::new |
path 中的 ed=N |
出站上的 WsStreamConfig |
不使用 early data | 由 WsTarget::new 从路径的查询串中解析;该限制被钳制到 MAX_EARLY_DATA(16 KiB),0 表示禁用 early data |
probe_interval、probe_timeout |
BalancerConfig |
DEFAULT_PROBE_INTERVAL(30 s)、DEFAULT_PROBE_TIMEOUT(5 s) |
无;两者都接受 0(见 负载均衡器) |
app 会给每条消息加上其上下文前缀。对于标签为 h 的入站,在 max_connections = 0 的配置上运行 etemenanki-app --test -c,会记录一条以 configuration invalid: inbound h: max_connections must be at least 1 结尾的错误行,并以失败状态退出。
每连接内存预算
Section titled “每连接内存预算”运行时在构造时一次性分配字节缓冲区,之后永不增长。因此一条连接的固定用户态开销是一些常量之和:
- 服务端运行时(
ProxyServerRuntime)分配三个大小为BUF_SIZE的堆上数组:up(传输层读取)、staging(发往传输层的字节)和scratch(出站读取)。 - 每个流式协议核心还拥有一个
SniffPrefix,其Collector::new调用Vec::with_capacity(SNIFF_LIMIT)。这是每连接 4 KiB,在构建协议核心时分配,无论嗅探是否开启。SniffPrefix::clear会用一个新的Collector::new()替换 collector,因此这 4 KiB 在连接的整个生命周期内都保持分配。TunUdpCore和Hy2UdpCore没有SniffPrefix。 - 客户端运行时(
ProxyClientRuntime)分配两个大小为其BUF_SIZE的堆上数组:staging(发往上游的已 seal 字节)和down(来自上游的线上字节)。在拨号完成之前,它还持有装箱的拨号 future。
路由到代理出站的连接要为两个运行时都付出开销。路由到 freedom 的连接只付服务端这一侧,因为该出站是一个内核 socket。
服务端协议核心
Section titled “服务端协议核心”| 协议核心 | 承载内容 | BUF_SIZE |
按什么确定大小 | STAGING_RESERVE |
MAX_DATAGRAM |
datagram_limit() |
3 × BUF_SIZE |
含 SniffPrefix |
|---|---|---|---|---|---|---|---|---|
HttpCore |
一条 HTTP 入站连接 | 64 KiB(MAX_HEAD) |
一个完整的请求头部 | 256 | 4096(默认) | 仅流式 | 192 KiB | 196 KiB |
TrojanCore |
一条 Trojan 入站连接 | 16 KiB | 一个 UDP 帧:头部加 8 KiB | 808 | 8192(MAX_LENGTH) |
8192 | 48 KiB | 52 KiB |
VlessCore |
一条 VLESS 入站连接 | 16 KiB | 长度前缀之后的一个 MAX_DATAGRAM 大小的 UDP 帧 |
808 | 8192 | 8192 | 48 KiB | 52 KiB |
VMessCore |
一条 VMess 入站连接 | 32 KiB | 源码中未说明 | 4096 | 8192 | 8192 | 96 KiB | 100 KiB |
ShadowsocksCore |
一条 Shadowsocks 入站连接 | 20 KiB | 一个 MAX_PAYLOAD chunk 加开销 |
128 | 4096(默认) | 仅流式 | 60 KiB | 64 KiB |
Ss2022Core |
一条 Shadowsocks 2022 入站连接 | 32 KiB | 一个带填充的请求 chunk,或一个 record | 256 | 4096(默认) | 仅流式 | 96 KiB | 100 KiB |
Hy2StreamCore |
一条 Hysteria 2 代理流 | 8 KiB | 带最长地址和填充的请求 | 2048 | 4096(默认) | 仅流式 | 24 KiB | 28 KiB |
Hy2UdpCore |
一个 Hysteria 2 连接的全部数据报 | 16 KiB | 一个重组后的数据包加分片头部 | 4096 | 4096(MAX_UDP_SIZE) |
4096 | 48 KiB | 48 KiB(无 prefix) |
PassthroughCore |
一条 TUN TCP 流 | 8 KiB | “every buffer size this core needs” | 0 | 4096(默认) | 仅流式 | 24 KiB | 28 KiB |
TunUdpCore |
一个 TUN UDP 关联(一个客户端源) | 8 KiB | 一个数据报加预留量 | 4096 | 4096 | 4096 | 24 KiB | 24 KiB(无 prefix) |
非字面量的预留量是这样算出来的:
- Trojan:
PACKET_HEADER_MAX.next_multiple_of(16) + downlink_overhead(Self::BUF_SIZE)。PACKET_HEADER_MAX为259 + 2 + 2 = 263,向上取整到 272;downlink_overhead(16 * 1024)为2 * 268 = 536,合计 808。 - VLESS:
272 + downlink_overhead(Self::BUF_SIZE),同样是 808。 - Shadowsocks:
32 + 2 * CHUNK_OVERHEAD + 28,其中CHUNK_OVERHEAD = 34,结果为 128。 - Shadowsocks 2022:
32 + 1 + 8 + 32 + 2 + 2 * TAG_SIZE + RECORD_OVERHEAD + 115,其中TAG_SIZE = 16、RECORD_OVERHEAD = 34,结果为 256。
就地打开帧的协议核心要求每个帧都能放进 BUF_SIZE,更大的帧会让连接以 FrameTooLarge 结束。对 Ss2022Core 来说,这意味着会话建立之后 record 载荷不能超过 32,734 字节(32 KiB 减去 34 字节的 RECORD_OVERHEAD),尽管协议允许最多 0xFFFF。对 ShadowsocksCore 来说,这意味着 chunk 载荷不能超过 20,446 字节(20 KiB 减去 34 字节的 CHUNK_OVERHEAD)。
Hysteria 2 监听器每条代理流占用 28 KiB;启用 udp 时,每个连接再加 48 KiB,数据报运行时要等连接认证之后才启动。
SOCKS 入站
Section titled “SOCKS 入站”SOCKS 入站是唯一不运行协议核心的流式协议,因此它有自己的预算:
| SOCKS 路径 | 缓冲区 | 合计 |
|---|---|---|
CONNECT |
BidirectionalConnection<A, B, RELAY_BUF>:每个方向一个 16 KiB 缓冲区,在中继开始时分配。开启嗅探且目标为 IP 时,collect_prefix 先持有一个 4 KiB 的读取块和一个容量为 4 KiB 的 Collector(中继建立之前共 8 KiB)。收集到的前缀在中继的剩余时间里一直留在 connect 的作用域中。 |
32 KiB,嗅探后为 36 KiB |
UDP ASSOCIATE |
up 和 down,各为 HUB_BUF(64 KiB),外加一个 2 KiB 的数据包 BytesMut 和一个 256 字节的 sink 数组。associate 只在 ExpectedSender::new 接受了请求并且中继 socket 已绑定之后才分配它们,因此以应答 0x02 拒绝的关联不占用其中任何一个。 |
约 130 KiB |
客户端 codec
Section titled “客户端 codec”| 出站 | Codec | 缓冲区常量 | STAGING_RESERVE |
2 × BUF |
|---|---|---|---|---|
| HTTP | HttpConnect |
HTTP_BUF = 16 KiB |
1024(REQUEST_MAX) |
32 KiB |
SOCKS(CONNECT) |
SocksConnect |
SOCKS_BUF = 16 KiB |
528(一个 513 字节的凭据消息,或一个带完整地址的请求) | 32 KiB |
| Trojan | TrojanStream、TrojanDatagram |
TROJAN_BUF = 16 KiB |
320(REQUEST_HEADER_MAX = 56 + 2 + 1 + 259 + 2) |
32 KiB |
| VLESS | VlessStream、VlessDatagram |
VLESS_BUF = 16 KiB |
278(REQUEST_HEADER_MAX = 1 + 16 + 1 + 1 + 259) |
32 KiB |
| VMess | VMessStream、VMessDatagram |
VMESS_BUF = 32 KiB |
384(HEADER_MAX = 358,向上取整到 64 的倍数) |
64 KiB |
| Shadowsocks | SsStream |
SS_BUF = 20 KiB |
386(32 + CHUNK_OVERHEAD + AddressCodec::MAX_LEN + 61) |
40 KiB |
| Shadowsocks 2022 | Ss2022Stream |
SS2022_BUF = 32 KiB |
2048 | 64 KiB |
不使用客户端运行时的出站:
| 出站 | 每条流的用户态缓冲区 |
|---|---|
freedom |
无;该出站是一个内核 socket。 |
blackhole |
无。 |
SOCKS UDP ASSOCIATE |
SocksUdpLink:一个 64 KiB 的 RECV_BUF 数组和一个 2 KiB 的 scratch BytesMut,约 66 KiB。 |
| WireGuard,TCP 流 | 两个大小为 TCP_BUFFER 的 smoltcp 环形缓冲区:128 KiB。通道中的条目占用这两个环之外的内存,但它们是有界的:上行方向最多 CHANNEL_CAP(256)个再加一个被持有的条目,下行方向最多 256 个,每个条目的大小等于产生它的那次写入或 socket 读取。设备另外一次性占用 2 × SCRATCH(128 KiB),由其所有流共享。 |
| WireGuard,UDP 流 | 两个 smoltcp 数据包缓冲区,各有 TCP_BUFFER 的载荷存储:128 KiB。通道中的条目另计,界限相同:上行方向最多 CHANNEL_CAP(256)个数据报再加一个被持有的,下行方向最多 256 个。 |
| Hysteria 2 | 没有固定缓冲区。QUIC 流量控制把在途数据限制在每条流 STREAM_RECEIVE_WINDOW、每个连接 CONNECTION_RECEIVE_WINDOW 以内。 |
| 入站 → 出站 | 服务端 | 客户端 | 大致合计 |
|---|---|---|---|
Trojan → freedom |
52 KiB | 0 | 52 KiB |
| VLESS → VLESS | 52 KiB | 32 KiB | 84 KiB |
| VMess → VMess | 100 KiB | 64 KiB | 164 KiB |
HTTP → freedom |
196 KiB | 0 | 196 KiB |
| Shadowsocks 2022 → WireGuard(TCP) | 100 KiB | 128 KiB | 228 KiB |
SOCKS CONNECT → Trojan |
32 KiB | 32 KiB | 64 KiB |
TUN TCP → freedom |
28 KiB | 0 | 28 KiB |
mux.cool 承载连接只为它的服务端运行时付一次开销。每条路由到代理出站的子流再各自加上自己的客户端运行时。因此,一个承载了最多 256 条子流、经由 VLESS 出站转发的 Trojan 承载连接,占用 52 KiB + 256 × 32 KiB = 8,244 KiB,约 8 MiB。
| 不变量 | 机制 | 由谁固定 |
|---|---|---|
| 运行时总能 stage 其预留量 | ProxyServerRuntime::build 中的 assert!(BUF_SIZE > Core::STAGING_RESERVE),ProxyClientRuntime::new 中的 assert!(BUF_SIZE > Codec::STAGING_RESERVE) |
测试中以各协议核心自己的 BUF_SIZE 构建的每个运行时;没有专门的 should_panic 测试 |
| 超过读缓冲区的帧是错误,而不是卡住 | up.is_saturated() 时 poll_transport_read 返回 FrameTooLarge |
concepts/tests/runtime.rs 中的 frame_larger_than_the_buffer_is_an_error |
| staging 背压绝不截断来自出站的数据报 | 数据报出站只在剩余空间达到 STAGING_RESERVE + datagram_limit() 时才被轮询 |
concepts/tests/runtime.rs 中的 datagram_outbound_is_never_truncated_by_staging_backpressure |
| 卡住的出站只卡住自己的上行,不影响其他下行 | effect 按顺序执行,被阻塞的转发会挂起传输层读取 | concepts/tests/runtime.rs 中的 stalled_outbound_holds_uplink_but_not_other_downlink |
| 客户端运行时的缓冲永远不超出它的两个数组 | poll_write 等待剩余空间达到 STAGING_RESERVE + 1 字节 |
concepts/tests/client.rs 中的 backpressure_from_the_wire_reaches_the_writer |
| 握手截止时间只设置一次,空闲截止时间在每个字节事件时设置 | Timing::touch |
protocols/tests/unit/core/mod.rs 中的 timing_arms_handshake_once_then_idle_per_byte_event |
| 截止时间按 tokio 时钟触发 | 运行时的计时器是一个 tokio Sleep,所以暂停的测试时钟可以推进它 |
concepts/tests/runtime.rs 中的 deadline_event_lets_the_core_time_out、deadline_is_armed_against_the_tokio_clock |
嗅探取用的数据绝不超过 SNIFF_LIMIT |
SniffPrefix::push 最多取 Collector::remaining() |
protocols/tests/unit/core/mod.rs 中的 sniff_prefix_takes_no_more_than_its_budget |
| mux 承载连接绝不持有超过 256 条子流 | Demux 以 End 拒绝 New |
protocols/tests/unit/mux/demux.rs 中的 unknown_or_excess_sessions_are_declined_with_end |
| gRPC 和 WebSocket 绝不缓冲超过 1 MiB 的消息 | MAX_GRPC_MESSAGE_LEN 检查在缓冲之前执行;ws_config() |
grpc_framing.rs 中的 decoder_rejects_an_oversized_length_before_buffering_the_body;ws_endpoint.rs 中的 the_configured_limits_replace_tungstenite_defaults |
| 拨号在失败前会尝试每一个地址 | TcpDialer::connect_any |
environment/tests/integration/tcp.rs 中的 connect_any_falls_through_a_dead_address |
| 面对大量不同名称时 DNS 缓存保持有界 | moka max_capacity(CACHE_CAPACITY) |
protocols/tests/unit/dns/mod.rs 中的 the_cache_stays_bounded_under_distinct_names |
| 重连退避会增长并有上限 | note_failure 最多移位 5 次,然后取 min(MAX) |
backoff_grows_with_each_failure_and_is_capped(Hysteria)、repeated_failures_escalate_the_backoff(WireGuard) |
| SOCKS UDP 关联的缓冲区和空闲计时器只为它自己的客户端服务 | ExpectedSender::admits 在 parse_udp_packet 之前、也在重置空闲计时器之前运行:每个数据报都必须来自控制连接的 IP;第一个被转发的数据报固定端口,除非请求中已经用该 IP 给出了端口。经 Unix socket 时,请求必须给出确切的 IP 和端口 |
protocols/tests/pipeline/socks.rs 中的 udp_association_ignores_another_ip(仅 Linux)和 udp_association_ignores_another_port_once_pinned;protocols/tests/unit/socks/server.rs 中的 only_the_control_peer_is_heard_and_its_first_datagram_pins_the_port |
| Hysteria UDP 会话计入 circuit 预算 | Hy2UdpCore 为每个会话获取一个 circuit_permits 许可 |
protocols/tests/unit/hysteria/server/datagrams.rs 中的 the_circuit_limit_refuses_new_sessions |
| WireGuard 驱动为每条连接最多持有一个上行条目;卡住的隧道流只阻塞它自己的写入方,不影响同一隧道上的其他流 | Uplink 最多持有一个 socket 尚未接受的条目,且 poll_refill 只在没有持有条目时才读取通道,因此 socket 写满后有界通道随之填满,应用侧的发送方进入等待 |
protocols/tests/pipeline/wireguard.rs 中的 a_stalled_tcp_flow_blocks_its_writer(以 1 KiB 分块写入的写入方必须在写入 512 KiB 之前进入等待,预期值为 385 KiB,同时同一隧道上的另一条流仍能回显);protocols/tests/unit/wireguard/device.rs 中的 a_held_item_keeps_the_channel_unread |
失败路径与取消
Section titled “失败路径与取消”限制触发时,结果属于以下四类之一。知道是哪一类,就知道该去日志的什么地方找。
| 类别 | 限制 | 对端看到什么 | 日志 |
|---|---|---|---|
| 准入时静默丢弃 | MAX_LIVE_CONNECTIONS_PER_INBOUND、MAX_HANDSHAKES_PER_INBOUND、TUN max_flows 和 FLOW_QUEUE、Hysteria UDP MAX_SESSIONS、Hysteria 数据报的 circuit 限制 |
socket 被关闭,或数据包丢失 | debug 或 trace |
| 明确拒绝 | Hysteria max_connections(incoming.refuse())、Hysteria 流上的 circuit 限制(H3_REQUEST_REJECTED)、MAX_CLASSIFYING_STREAMS(H3_EXCESSIVE_LOAD)、mux MAX_SESSIONS(一个 End 帧)、WebSocket early data(413) |
协议层面的拒绝;如果有承载连接,它会保留下来 | debug;mux 的拒绝不记录日志 |
| 连接错误 | FrameTooLarge、HANDSHAKE_TIMEOUT、TRANSPORT_HANDSHAKE_TIMEOUT、MAX_GRPC_MESSAGE_LEN、MAX_WS_MESSAGE_LEN、解析器的帧上限、SOCKS 中继空闲检查 |
连接关闭 | debug。对流式入站,传输层失败是 serve_socket 输出的 inbound transport failed: <error>,运行时或驱动错误是 serve_connection 输出的 <protocol> connection from <source> ended: <error>,其中源地址以 Option 形式打印 |
| 正常结束 | 协议核心中的 RELAY_IDLE_TIMEOUT、WS_IDLE_TIMEOUT、H2_IDLE_TIMEOUT、PING 超时、QUIC MAX_IDLE_TIMEOUT、Hysteria UDP 清扫、SOCKS UDP ASSOCIATE 空闲计时器 |
流结束,就像对端离开了一样 | 通常没有 |
取消不依赖这些计时器中的任何一个。流式入站在 generation 的 CancellationToken 下用 spawn_scoped 启动每个连接的 task。Hysteria 2 和 TUN 入站则在其 run future 内部用一个 JoinSet 持有每个连接的工作。无论哪种方式,结束一个 generation 都会丢弃每个运行时,连同它的缓冲区和许可。随后,Hysteria 2 和 TUN 入站分别最多等待 DRAIN_TIMEOUT 和 RELEASE_TIMEOUT,让它们的 socket 或设备被释放。参见 Generation 与热重载。
| 测试 | 文件 | 固定的行为 |
|---|---|---|
frame_larger_than_the_buffer_is_an_error |
concepts/tests/runtime.rs |
FrameTooLarge |
datagram_outbound_is_never_truncated_by_staging_backpressure |
concepts/tests/runtime.rs |
datagram_limit() 及数据报读取条件 |
stalled_outbound_holds_uplink_but_not_other_downlink |
concepts/tests/runtime.rs |
以有序 effect 实现的背压 |
deadline_event_lets_the_core_time_out、deadline_is_armed_against_the_tokio_clock |
concepts/tests/runtime.rs |
协议核心唯一的截止时间 |
backpressure_from_the_wire_reaches_the_writer |
concepts/tests/client.rs |
客户端运行时的 staging 上界 |
connect_any_falls_through_a_dead_address |
environment/tests/integration/tcp.rs |
逐个地址顺序拨号 |
timing_arms_handshake_once_then_idle_per_byte_event、timing_reports_handshake_and_sniff_expiry_to_the_core |
protocols/tests/unit/core/mod.rs |
Timing |
sniff_prefix_takes_no_more_than_its_budget、a_sniffing_passthrough_core_opens_on_the_sniff_deadline、passthrough_core_finishes_when_the_outbound_fails_or_idles |
protocols/tests/unit/core/mod.rs |
SNIFF_LIMIT、SNIFF_TIMEOUT、RELAY_IDLE_TIMEOUT |
handshake_deadline_fails_the_connection、sniff_deadline_opens_with_what_was_collected |
protocols/tests/unit/trojan/core.rs |
真实协议核心中的握手与嗅探截止时间 |
the_budget_ends_the_search_without_a_match |
protocols/tests/unit/sniff/collector.rs |
SNIFF_LIMIT |
a_connection_that_opens_no_stream_is_given_up_on |
protocols/tests/unit/transports/accept.rs |
H2_IDLE_TIMEOUT,端到端 |
liveness_gives_up_on_a_connection_with_no_streams、liveness_keeps_a_connection_carrying_streams、liveness_restarts_the_idle_deadline_on_progress |
protocols/tests/unit/transports/grpc_liveness.rs |
Liveness |
decoder_accepts_a_message_at_the_maximum、decoder_rejects_a_message_longer_than_the_maximum、decoder_rejects_an_oversized_length_before_buffering_the_body |
protocols/tests/unit/transports/grpc_framing.rs |
MAX_GRPC_MESSAGE_LEN |
rejects_oversized_early_data_header、callback_rejects_oversized_early_data、the_configured_limits_replace_tungstenite_defaults |
protocols/tests/unit/transports/ws_endpoint.rs |
MAX_EARLY_DATA、MAX_WS_MESSAGE_LEN |
unknown_or_excess_sessions_are_declined_with_end |
protocols/tests/unit/mux/demux.rs |
Mux MAX_SESSIONS |
read_meta_enforces_the_length_caps |
protocols/tests/unit/mux/frame.rs |
MAX_META_LEN、MAX_DATA_LEN |
an_oversized_head_is_refused |
protocols/tests/unit/http/core.rs |
MAX_HEAD |
udp_association_ignores_another_ip、udp_association_ignores_another_port_once_pinned、udp_association_refuses_a_relay_that_cannot_hear_the_client、udp_link_ignores_datagrams_not_from_the_relay |
protocols/tests/pipeline/socks.rs |
SOCKS UDP ASSOCIATE 中继(HUB_BUF)和 SocksUdpLink(RECV_BUF)处理哪些数据报,以及对中继永远收不到其客户端的关联以 0x02 拒绝 |
only_the_control_peer_is_heard_and_its_first_datagram_pins_the_port、a_relay_that_cannot_hear_the_client_is_refused |
protocols/tests/unit/socks/server.rs |
ExpectedSender 和 hears |
rejects_oversize_udp_packet |
protocols/tests/unit/trojan/protocol.rs |
Trojan MAX_LENGTH |
user_not_found_outside_time_window、replayed_authid_is_rejected、expired_ids_leave_the_set |
protocols/tests/unit/vmess/accounts.rs |
AUTHID_WINDOW_SECS、REPLAY_TTL |
an_idle_session_is_closed_by_the_sweep、the_circuit_limit_refuses_new_sessions |
protocols/tests/unit/hysteria/server/datagrams.rs |
SWEEP_INTERVAL、circuit 许可 |
reassembly_stops_at_the_maximum_datagram_size、a_payload_needing_more_than_255_fragments_is_refused、sanitise_message_caps_length_and_tolerates_bad_utf8 |
protocols/tests/unit/hysteria/protocol.rs |
MAX_UDP_SIZE、分片、MAX_MESSAGE_KEPT |
backoff_grows_with_each_failure_and_is_capped、a_connection_that_never_started_counts_as_dying_young、a_backing_off_slot_refuses_without_dialling |
protocols/tests/unit/hysteria/slot.rs |
重连退避、MIN_HEALTHY_LIFETIME |
repeated_failures_escalate_the_backoff、a_short_lived_tunnel_counts_as_a_failure、the_backoff_gate_refuses_without_building_another_tunnel |
protocols/tests/unit/wireguard/slot.rs |
重建退避、MIN_HEALTHY_LIFETIME |
a_held_item_keeps_the_channel_unread、the_uplink_finishes_only_after_its_last_item |
protocols/tests/unit/wireguard/device.rs |
Uplink:只持有一个条目,其后的通道保持不读,并且只有在最后一个条目交出之后才看到上行结束 |
a_stalled_tcp_flow_blocks_its_writer、an_oversized_datagram_does_not_wedge_the_association |
protocols/tests/pipeline/wireguard.rs |
卡住的 TCP 流上的 CHANNEL_CAP 界限;丢弃大于 TCP_BUFFER 发送环的数据报 |
the_association_finishes_when_the_outbound_fails_or_idles |
protocols/tests/unit/tun/udp.rs |
TUN UDP 关联空闲 |
the_cache_stays_bounded_under_distinct_names、an_expired_entry_is_looked_up_again |
protocols/tests/unit/dns/mod.rs |
CACHE_CAPACITY、TTL 过期 |
an_out_of_range_udp_idle_timeout_is_refused、a_udp_idle_timeout_without_udp_is_refused、a_zero_limit_is_refused、tun_refuses_an_mtu_below_the_stack_floor |
app/tests/unit/inbound.rs |
限制的配置校验 |
hysteria2_refuses_only_a_zero_stream_limit、the_default_stream_limit_is_configurable_as_well_as_defaulted |
app/tests/unit/outbound.rs |
max_concurrent_streams |
accept_error_backoff_classification |
app/tests/unit/serve.rs |
哪些 accept 错误会触发退避 |
every_member_down_still_selects_rather_than_dropping |
app/tests/unit/balancer.rs |
负载均衡器回退 |
traffic_moves_off_a_member_that_stops_answering |
app/tests/integration/e2e_balancer.rs |
探测间隔与超时,端到端 |
one_association_routes_each_peer_separately、replies_from_several_peers_merge_back_correctly |
app/tests/integration/e2e_udp_route.rs |
UDP fan-out |
在当前版本中,以下限制没有以其名称命名的测试:WORK_BUDGET、MAX_SUBS 淘汰、MAX_LIVE_CONNECTIONS_PER_INBOUND、MAX_HANDSHAKES_PER_INBOUND、TRANSPORT_HANDSHAKE_TIMEOUT、TCP keepalive 参数、WS_KEEPALIVE_INTERVAL 和 WS_IDLE_TIMEOUT、downlink_overhead,以及客户端侧的 Hysteria 限制(OPEN_STREAM_TIMEOUT、MAX_UDP_SESSIONS、UDP_SESSION_BACKLOG)。
修改一个限制
Section titled “修改一个限制”-
找出由它派生的每个值。 搜索该常量的名字。预留量常常由其他常量计算得出(
downlink_overhead(Self::BUF_SIZE)、REQUEST_HEADER_MAX、AddressCodec::MAX_LEN),所以调大某个协议核心的BUF_SIZE可能会连带调大它自己的预留量。 -
让预留量保持小于缓冲区。
BUF_SIZE必须始终大于STAGING_RESERVE,否则运行时在构建时 panic。对于数据报协议核心,还要确认BUF_SIZE - STAGING_RESERVE不小于MAX_DATAGRAM,否则datagram_limit()会悄悄变小,数据包会被截断。 -
按最大的完整帧确定协议核心的
BUF_SIZE。 协议核心必须完整看到的帧(HTTP 头部、Shadowsocks chunk 或 record、长度前缀之后的 UDP 帧)必须放得进BUF_SIZE,否则连接以FrameTooLarge结束。客户端缓冲区则按其 codec 要打开的最大线上帧加上 codec 的预留量来确定。 -
更新内存预算。 服务端缓冲区的开销是变化量的三倍,客户端缓冲区是两倍。更新本页的表格;如果是运维人员可见的默认值,也要更新 用户指南。
-
用测试固定它。 在测试中按名字引用该常量,而不是重复字面量,就像
the_cache_stays_bounded_under_distinct_names对CACHE_CAPACITY的做法那样。然后运行 测试 中的各项检查。