跳转到内容

限制、超时与内存

源码文件:108 个 · 核对版本 Etemenanki 596916d
  • Etemenanki/concepts/src/core.rs
  • Etemenanki/concepts/src/runtime.rs
  • Etemenanki/concepts/src/client.rs
  • Etemenanki/concepts/src/buffer.rs
  • Etemenanki/concepts/src/relay.rs
  • Etemenanki/concepts/src/wake.rs
  • Etemenanki/protocols/src/core/mod.rs
  • Etemenanki/protocols/src/core/harness.rs
  • Etemenanki/protocols/src/sniff/mod.rs
  • Etemenanki/protocols/src/sniff/collector.rs
  • Etemenanki/protocols/src/transports/accept.rs
  • Etemenanki/protocols/src/transports/connect.rs
  • Etemenanki/protocols/src/transports/keepalive.rs
  • Etemenanki/protocols/src/transports/ws/endpoint.rs
  • Etemenanki/protocols/src/transports/ws/stream.rs
  • Etemenanki/protocols/src/transports/grpc/settings.rs
  • Etemenanki/protocols/src/transports/grpc/liveness.rs
  • Etemenanki/protocols/src/transports/grpc/framing.rs
  • Etemenanki/environment/src/dial/tcp.rs
  • Etemenanki/environment/src/dial/socket.rs
  • Etemenanki/protocols/src/mux/demux.rs
  • Etemenanki/protocols/src/mux/frame.rs
  • Etemenanki/protocols/src/helpers/address.rs
  • Etemenanki/protocols/src/http/protocol.rs
  • Etemenanki/protocols/src/http/core.rs
  • Etemenanki/protocols/src/http/codec.rs
  • Etemenanki/protocols/src/socks/server.rs
  • Etemenanki/protocols/src/socks/protocol.rs
  • Etemenanki/protocols/src/socks/codec.rs
  • Etemenanki/protocols/src/socks/udp_link.rs
  • Etemenanki/protocols/src/trojan/protocol.rs
  • Etemenanki/protocols/src/trojan/core.rs
  • Etemenanki/protocols/src/trojan/codec.rs
  • Etemenanki/protocols/src/vless/protocol.rs
  • Etemenanki/protocols/src/vless/core.rs
  • Etemenanki/protocols/src/vless/codec.rs
  • Etemenanki/protocols/src/vmess/core.rs
  • Etemenanki/protocols/src/vmess/codec.rs
  • Etemenanki/protocols/src/vmess/framing.rs
  • Etemenanki/protocols/src/vmess/accounts.rs
  • Etemenanki/protocols/src/ss_legacy/aead.rs
  • Etemenanki/protocols/src/ss_legacy/core.rs
  • Etemenanki/protocols/src/ss_legacy/codec.rs
  • Etemenanki/protocols/src/ss_2022/crypto.rs
  • Etemenanki/protocols/src/ss_2022/protocol.rs
  • Etemenanki/protocols/src/ss_2022/core.rs
  • Etemenanki/protocols/src/ss_2022/codec.rs
  • Etemenanki/protocols/src/hysteria/config.rs
  • Etemenanki/protocols/src/hysteria/protocol.rs
  • Etemenanki/protocols/src/hysteria/connection.rs
  • Etemenanki/protocols/src/hysteria/slot.rs
  • Etemenanki/protocols/src/hysteria/server/config.rs
  • Etemenanki/protocols/src/hysteria/server/endpoint.rs
  • Etemenanki/protocols/src/hysteria/server/inbound.rs
  • Etemenanki/protocols/src/hysteria/server/datagrams.rs
  • Etemenanki/protocols/src/hysteria/server/shim.rs
  • Etemenanki/protocols/src/hysteria/obfs.rs
  • Etemenanki/protocols/src/wireguard/config.rs
  • Etemenanki/protocols/src/wireguard/device.rs
  • Etemenanki/protocols/src/wireguard/slot.rs
  • Etemenanki/protocols/src/tun/config.rs
  • Etemenanki/protocols/src/tun/inbound.rs
  • Etemenanki/protocols/src/tun/udp.rs
  • Etemenanki/protocols/src/dns/mod.rs
  • Etemenanki/protocols/src/dns/message.rs
  • Etemenanki/app/src/serve.rs
  • Etemenanki/app/src/connector.rs
  • Etemenanki/app/src/outbound/mod.rs
  • Etemenanki/app/src/outbound/proxy.rs
  • Etemenanki/app/src/outbound/freedom.rs
  • Etemenanki/app/src/outbound/udp_fanout.rs
  • Etemenanki/app/src/balancer.rs
  • Etemenanki/app/src/instance.rs
  • Etemenanki/app/src/inbound/mod.rs
  • Etemenanki/app/src/inbound/tun.rs
  • Etemenanki/app/src/config.rs
  • Etemenanki/app/src/main.rs
  • Etemenanki/concepts/tests/runtime.rs
  • Etemenanki/concepts/tests/client.rs
  • Etemenanki/environment/tests/integration/tcp.rs
  • Etemenanki/protocols/tests/unit/core/mod.rs
  • Etemenanki/protocols/tests/unit/sniff/collector.rs
  • Etemenanki/protocols/tests/unit/transports/accept.rs
  • Etemenanki/protocols/tests/unit/transports/grpc_framing.rs
  • Etemenanki/protocols/tests/unit/transports/grpc_liveness.rs
  • Etemenanki/protocols/tests/unit/transports/ws_endpoint.rs
  • Etemenanki/protocols/tests/unit/mux/demux.rs
  • Etemenanki/protocols/tests/unit/mux/frame.rs
  • Etemenanki/protocols/tests/unit/http/core.rs
  • Etemenanki/protocols/tests/unit/socks/server.rs
  • Etemenanki/protocols/tests/pipeline/socks.rs
  • Etemenanki/protocols/tests/unit/trojan/protocol.rs
  • Etemenanki/protocols/tests/unit/trojan/core.rs
  • Etemenanki/protocols/tests/unit/vmess/accounts.rs
  • Etemenanki/protocols/tests/unit/hysteria/protocol.rs
  • Etemenanki/protocols/tests/unit/hysteria/slot.rs
  • Etemenanki/protocols/tests/unit/hysteria/server/datagrams.rs
  • Etemenanki/protocols/tests/unit/wireguard/slot.rs
  • Etemenanki/protocols/tests/unit/wireguard/device.rs
  • Etemenanki/protocols/tests/pipeline/wireguard.rs
  • Etemenanki/protocols/tests/unit/tun/udp.rs
  • Etemenanki/protocols/tests/unit/dns/mod.rs
  • Etemenanki/app/tests/unit/inbound.rs
  • Etemenanki/app/tests/unit/outbound.rs
  • Etemenanki/app/tests/unit/serve.rs
  • Etemenanki/app/tests/unit/balancer.rs
  • Etemenanki/app/tests/integration/e2e_udp_route.rs
  • Etemenanki/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。

BUF_SIZE 是两个运行时的 const 泛型参数:

concepts/src/runtime.rs
pub struct ProxyServerRuntime<const BUF_SIZE: usize, Core, Trans, Conn, Mode = ProxyRunsQuiet>
where
Core: ProxyCoreDecode,
Conn: Connector<Core::Target>,
concepts/src/client.rs
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 项:

concepts/src/core.rs
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 的截断方式相同。

运行时把后两者换算成它从数据报出站读取的最大数据报:

concepts/src/runtime.rs
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 头部总能放得下:

protocols/src/mux/demux.rs
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 值中:

protocols/src/core/mod.rs
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 还在运行时之外监视握手:

app/src/serve.rs
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;
}
protocols/src/transports/keepalive.rs
pub fn set_keepalive(stream: &TcpStream)
// environment/src/dial/tcp.rs
impl 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.rs
impl SocketOptions {
pub fn with_tcp_keepalive(mut self, idle: Duration) -> Self
}
// protocols/src/transports/ws/endpoint.rs
pub(crate) fn ws_config() -> tokio_tungstenite::tungstenite::protocol::WebSocketConfig
// protocols/src/transports/grpc/liveness.rs
impl 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.rs
impl 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) -> bool
pub fn note_failure(&mut self)
// app/src/balancer.rs
impl 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 中每个字节事件都会重新设置。这使它成为空闲限制,而不是生命周期限制。

数值都是精确值。源码写作 n * 1024 的地方,这里以 KiB 或 MiB 给出。

常量 取值 定义位置 执行机制
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。

常量 取值 定义位置 执行机制
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 预留量的一部分。

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),触发它的那个数据包不会被送达。

服务端:

常量 取值 定义位置 执行机制
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),因此面对先认证再关闭的服务器时,退避会不断累积。
常量 取值 定义位置 执行机制
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 则让探测一次接一次地连续进行。

这些常量限制解析器接受什么、编码器产出什么,或者协议驱动缓冲多少数据。解析器上限会让帧在解析处失败。没有解析器上限的帧只受它必须放入的缓冲区约束,放不下时连接以 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 同上 同上

只有下面这些限制从配置文件读取。本页其他所有内容都是编译进去的。

配置键 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 结尾的错误行,并以失败状态退出。

运行时在构造时一次性分配字节缓冲区,之后永不增长。因此一条连接的固定用户态开销是一些常量之和:

  • 服务端运行时(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。

协议核心 承载内容 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 入站是唯一不运行协议核心的流式协议,因此它有自己的预算:

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 缓冲区常量 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

限制触发时,结果属于以下四类之一。知道是哪一类,就知道该去日志的什么地方找。

类别 限制 对端看到什么 日志
准入时静默丢弃 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)。

  1. 找出由它派生的每个值。 搜索该常量的名字。预留量常常由其他常量计算得出(downlink_overhead(Self::BUF_SIZE)、REQUEST_HEADER_MAX、AddressCodec::MAX_LEN),所以调大某个协议核心的 BUF_SIZE 可能会连带调大它自己的预留量。

  2. 让预留量保持小于缓冲区。 BUF_SIZE 必须始终大于 STAGING_RESERVE,否则运行时在构建时 panic。对于数据报协议核心,还要确认 BUF_SIZE - STAGING_RESERVE 不小于 MAX_DATAGRAM,否则 datagram_limit() 会悄悄变小,数据包会被截断。

  3. 按最大的完整帧确定协议核心的 BUF_SIZE。 协议核心必须完整看到的帧(HTTP 头部、Shadowsocks chunk 或 record、长度前缀之后的 UDP 帧)必须放得进 BUF_SIZE,否则连接以 FrameTooLarge 结束。客户端缓冲区则按其 codec 要打开的最大线上帧加上 codec 的预留量来确定。

  4. 更新内存预算。 服务端缓冲区的开销是变化量的三倍,客户端缓冲区是两倍。更新本页的表格;如果是运维人员可见的默认值,也要更新 用户指南。

  5. 用测试固定它。 在测试中按名字引用该常量,而不是重复字面量,就像 the_cache_stays_bounded_under_distinct_names 对 CACHE_CAPACITY 的做法那样。然后运行 测试 中的各项检查。