跳转到内容

限制与超时

etemenanki-app 为连接的每个阶段都设了上限:客户端完成认证最多可以花多久,中继最多可以空闲多久,一个监听器最多容纳多少连接,一条消息最大能有多大。这些值大多写死在代码中,并且远高于正常流量的需要。少数几个属于某个协议,可以通过设置修改。

当连接被关闭而你想知道是哪个计时器关闭了它、在规划服务器容量,或者需要确认 UDP 能否通过某个特定的入站和出站时,请阅读本页。每一节都会说明达到上限时会发生什么,以及日志中会出现什么。

共有十一个键可以修改限制。它们都属于某个协议或负载均衡器,并在对应页面中有完整说明。本页中的其他所有值都是固定的。

键 位置 默认值 可接受的值
max_connections hysteria2 入站的 settings 4096 1 及以上
max_circuits hysteria2 入站的 settings 65536 1 及以上
udp_idle_timeout hysteria2 入站的 settings 60(秒) 2 到 600,且仅在 udp = true 时可用
max_concurrent_streams hysteria2 出站的 settings 102400 1 及以上,无上限
mtu tun 入站的 settings 1500 1280 到 65535
udp_idle_timeout tun 入站的 settings 60(秒) 任意非负整数;不做范围检查,0 会使 UDP 无法工作
max_flows tun 入站的 settings 65536 任意非负整数;0 表示不接纳任何流
mtu wireguard 出站的 settings 1420 任意非负整数;不做范围检查
keepalive wireguard 出站的 settings 未设置(关闭) 0 到 65535 秒;0 表示关闭
probe_interval [[balancer]] 30(秒) 任意非负整数
probe_timeout [[balancer]] 5(秒) 任意非负整数

它们都是普通整数。时长以整秒表示,因此 probe_interval = "30s" 是类型错误,而不是 30 秒:

configuration invalid: TOML parse error at line 8, column 18
|
8 | probe_interval = "30s"
| ^^^^^
invalid type: string "30s", expected u64

超出整数类型范围的值同样是解析错误,例如在 TUN 入站上设置 mtu = 70000:inbound tun-in: invalid settings: invalid value: integer `70000`, expected u16。

默认值适用于大多数部署。下面这组配置显式设置了每一个可配置的限制,并在注释中说明了每个取值的理由。两个文件都能通过 etemenanki-app --test。

server.toml
# A Hysteria 2 server sized for a small group of users.
[[inbound]]
tag = "hy2-in"
protocol = "hysteria2"
listen = "0.0.0.0"
port = 443
[inbound.settings]
cert_file = "/etc/etemenanki/cert.pem"
key_file = "/etc/etemenanki/key.pem"
password = "replace-with-a-long-random-password"
udp = true
udp_idle_timeout = 120 # keep quiet UDP sessions for 2 minutes; 2 to 600
max_connections = 512 # QUIC connections; default 4096
max_circuits = 16384 # TCP streams plus UDP associations; default 65536
[[outbound]]
tag = "direct"
protocol = "freedom"

经过流式入站(SOCKS、HTTP、Trojan、VLESS、VMess 或 Shadowsocks)的 TCP 连接会依次经历以下阶段,每个阶段都有各自的上限:

flowchart LR
  A["接受:有空闲的握手和连接名额"] --> T["传输层握手:10 秒"]
  T --> P["协议请求:10 秒"]
  P --> S["嗅探:300 毫秒或 4 KiB"]
  S --> D["拨号:DNS 每次查询 5 秒,TCP 连接每个地址 10 秒"]
  D --> R["中继:空闲 300 秒后关闭"]
  R -.-> K["对端存活检测:TCP keepalive、WebSocket 和 HTTP/2 ping"]
  • 传输层握手仅在入站配置了 TLS、WebSocket 或 gRPC 传输层时存在。普通 TCP 入站直接进入协议请求阶段。
  • 嗅探仅在开启了 sniffing 且客户端请求的是 IP 地址时运行。见入站。
  • 协议请求的时限在入站拿到目标地址后立即结束。在 HTTP、Trojan、VLESS、VMess 和 Shadowsocks 入站上,连接随后就受中继空闲时限约束,出站拨号期间也是如此。
  • 对端存活检测在连接的整个生命周期内运行。它发现的是没有关闭连接就消失的对端,而不是空闲但仍会应答的对端。

Hysteria 2 和 TUN 入站接受连接的方式不同,有各自的限制,见 Hysteria 2 和 TUN。

超时 取值 适用范围 结果
传输层握手 10 秒 network = "tls"、"ws" 或 "grpc" 的入站:TLS 握手、WebSocket 升级或 HTTP/2 preface 套接字关闭。debug 日志:inbound transport failed: tls handshake timed out,其中的 tls 也可能是 websocket 或 grpc
协议请求,静默 10 秒 HTTP、Trojan、VLESS、VMess 和 Shadowsocks 入站:请求完成前任意连续 10 秒没有进展 连接关闭:inbound handshake timed out after 10s
协议请求,总时长 从客户端第一个字节起 10 秒 同上 连接关闭:client did not complete its request in time
SOCKS 协商 10 秒 SOCKS 入站:问候、认证和请求合计 连接关闭:client did not complete its request in time
TUN 首字节 10 秒 TUN 入站上的 TCP 流。只有客户端先发送数据后才会拨号,因此由服务端先发言的协议(例如 SMTP 或 MySQL)总会在这里结束 流关闭:tun: the client never spoke
嗅探窗口 300 毫秒或 4 KiB,以先到者为准 开启 sniffing 且目标为 IP 地址的流 按地址路由该流
DNS 查询 每次查询 5 秒 [dns] 的 udp、tls 和 https 后端;A 和 AAAA 并行查询,各自有 5 秒 该查询失败,报 dns: query timed out;如果另一查询返回了地址,解析仍然成功
TCP 连接 每个地址 10 秒 freedom,以及 socks、http、trojan、vless、vmess 和 shadowsocks 出站到服务器的连接 尝试下一个解析出的地址
Hysteria 2 连接 每个地址 10 秒 hysteria2 出站:QUIC 握手和认证 尝试下一个解析出的地址。全部无应答时:hysteria2: no address answered (...)
Hysteria 2 打开流 5 秒 hysteria2 出站:在共享连接上打开一个代理流 该流失败:hysteria2: timed out opening a proxy stream
隧道内 TCP 连接 每个地址 10 秒 wireguard 出站:隧道内的 TCP 连接 尝试下一个地址
负载均衡器探测 probe_timeout,默认 5 秒 [[balancer]] 的每个成员,包含域名解析 该成员视为不可用

两行协议请求超时是配合使用的:客户端连上后一言不发,10 秒后被断开;客户端开始发送请求后,从第一个字节起有 10 秒时间完成请求。因此传输层握手完成后,一个缓慢的客户端在认证前最多能占用一条连接约 20 秒。这两条消息都以 debug 级别记录为连接结束的原因,例如 vless connection from Some(198.51.100.20) ended: inbound handshake timed out after 10s。

system DNS 后端本身没有超时:getaddrinfo 使用主机解析器的超时和重试设置。

地址是依次尝试的,而不是并发竞速。一个解析出三个不可达地址的域名需要 30 秒才会失败,错误信息会列出每一次尝试:

failed to connect to any address (192.0.2.10:443: connect to 192.0.2.10:443 timed out; 192.0.2.11:443: connect to 192.0.2.11:443 timed out; 192.0.2.12:443: connect to 192.0.2.12:443 timed out)

如果某个域名带有你无法到达的地址,例如在没有可用 IPv6 的主机上遇到 AAAA 记录,请在出站上设置 address_family 以跳过它。见出站。

计时器 取值 适用范围 结果
中继空闲 300 秒 HTTP、Trojan、VLESS、VMess 和 Shadowsocks 入站上的连接(mux.cool 为承载连接)、Hysteria 2 流,以及 TUN 的 TCP 流和 UDP 关联:两个方向都 300 秒没有读到数据 连接关闭
SOCKS 中继空闲 每 300 秒检查一次 SOCKS CONNECT 中继 自上次检查以来没有传输任何字节的中继关闭,报 socks: relay idle,发生在最后一个字节之后 300 到 600 秒之间
SOCKS UDP 关联空闲 300 秒 SOCKS UDP ASSOCIATE:两个方向都没有数据报 关联结束
TCP keepalive 静默 120 秒后发出第一个探测,之后每 30 秒一次;3 个探测无应答则断开连接 入站接受的每条 TCP 连接,以及出站拨往其代理服务器的每条连接 对端消失约 210 秒后被发现
WebSocket ping 60 秒内没有收发任何帧后 WebSocket 会话,两端均适用 发送一个 Ping
WebSocket 空闲 300 秒没有收发任何帧 WebSocket 会话,两端均适用 会话被视为已关闭
HTTP/2 ping 每 60 秒一次;PONG 必须在 20 秒内到达 gRPC 入站连接 HTTP/2 连接及其所有流关闭
HTTP/2 空闲 300 秒没有打开的流 gRPC 入站连接 HTTP/2 连接关闭
QUIC 空闲 30 秒 Hysteria 2,两端均适用 QUIC 连接关闭
QUIC keepalive 每 10 秒 hysteria2 出站 在空闲时保持连接存活
Hysteria 2 UDP 关联 udp_idle_timeout,默认 60 秒,每秒检查一次 设置了 udp = true 的 hysteria2 入站 关联结束
TUN UDP 流 udp_idle_timeout,默认 60 秒,两个方向都没有数据包 TUN 入站上一个客户端源地址与一个对端之间的通信 该流被回收。当某个客户端源地址的所有流都被回收后,它的关联结束并释放所占的 max_flows 名额
WireGuard keepalive keepalive,默认关闭 wireguard 出站 发送空数据包以保持 NAT 映射

“空闲”指没有载荷。每当有字节从客户端或出站到达,中继空闲计时器就会重新开始,因此自带 keepalive 的连接(例如 SSH 的 ServerAliveInterval,或隧道内的 WebSocket 应用层 ping)永远不会触发它。而五分钟内什么都不发送的长轮询或安静的数据库连接则会触发。

TCP keepalive 设置在代理接受的套接字和代理拨往代理服务器的套接字上。freedom 直接连往目标的连接使用操作系统默认值,在 Linux 上即不启用 keepalive。Unix 套接字监听器没有 TCP keepalive。

任何存活的对端都会应答 WebSocket 和 HTTP/2 ping,因此它们不会关闭仅仅是空闲的连接。在拨号一侧,经 gRPC 的每个流都会打开自己的 HTTP/2 连接,依靠 TCP keepalive 和中继空闲限制来处理。

情形 取值 说明
监听器 accept() 失败 暂停 100 毫秒 例如进程耗尽了文件描述符。日志显示 accept error, backing off 100ms: Too many open files (os error 24)
hysteria2 出站重连 2 秒,逐次翻倍,最多 30 秒 在连接失败后,或者连接存活不到 10 秒时。存活更久的连接会被立即替换。等待期间到达的流会失败,报 hysteria2: connection is down, waiting before the next attempt
wireguard 出站重建 2 秒,逐次翻倍,最多 30 秒 规则与 Hysteria 2 相同。等待期间到达的流会失败,报 wireguard: tunnel is down, waiting before the next attempt
负载均衡器探测 probe_interval,默认 30 秒 每个成员在每次探测结束后等待的时间

所有接受套接字的入站,即除 hysteria2 和 tun 以外的所有协议,无论使用 TCP 还是 Unix 套接字,都有两个固定限制,按每个入站分别计数:

限制 取值 计数对象 达到上限时
存活连接 65,536 已接受的套接字,从接受到关闭。一个 gRPC 连接无论承载多少流,都只计一次 新连接在被接受后立即关闭。debug 日志:dropping inbound connection; live connection limit reached
并发握手 2,048 仍处于握手阶段的连接 新连接在被接受后立即关闭。debug 日志:dropping inbound connection; handshake limit reached

这两者都是防御洪泛的护栏,而不是配额。实际上通常会先达到进程的打开文件数上限:每条被代理的连接为客户端占用一个描述符,通常还为出站占用一个。在 systemd 下,请按照运行 etemenanki-app 中的说明设置 LimitNOFILE=。

Trojan、VLESS 和 VMess 入站会自动接受 Xray 的 mux.cool 多路复用。

限制 取值 达到上限时
每条承载连接的子流数 256 新的子流立即结束;承载连接及其其他子流继续运行
每帧数据块 8 KiB,与 Xray 相同 承载连接关闭:mux: data length N exceeds 8192
帧元数据 512 字节,与 Xray 相同 承载连接关闭:mux: metadata length N exceeds 512

UDP 按数据包逐个路由,每个关联为它用过的每个出站保留一条子链路。一个关联最多持有 64 条子链路;第 65 条会关闭最久未发送过数据的那一条。只有在一个关联中用到超过 64 个不同出站时才会遇到这个限制。出站详细解释了扇出机制。

限制 取值 所在端 计数对象 达到上限时
max_connections 默认 4096 入站 监听器上的 QUIC 连接,无论是否已认证 拒绝 QUIC 握手。debug 日志:hysteria2: refusing a connection; the listener is full
max_circuits 默认 65,536 入站 监听器上所有连接中的 TCP 流与 UDP 关联之和 新流被重置(hysteria2: refusing a stream; the listener is at its circuit limit);新关联的数据包被丢弃
每个客户端连接的流数 1024 入站 一个 QUIC 连接上打开的双向流 客户端等待 QUIC 流额度
每个连接的 UDP 关联数 256 两端 一个 QUIC 连接上存活的 UDP 会话 入站:新会话的数据包被丢弃。出站:hysteria2: connection is at its UDP association limit
max_concurrent_streams 默认 102,400 出站 出站唯一的共享连接上打开的代理流,所有用户合计 流立即失败:hysteria2: connection is at its concurrent-stream limit
接收窗口 每个流 8 MiB,每个连接 20 MiB 两端 流量控制 发送方等待
重组后的 UDP 数据包 4096 字节 两端 分片最多重组到这个大小 丢弃该数据包

一个 circuit 在整个生命周期内都占用一个 max_circuits 名额,直到它的中继结束或关联超时。

两个流限制在出站上会相互影响。etemenanki-app 服务端为每个连接授予 1024 个流。在默认的 max_concurrent_streams = 102400 下,出站自身永远不会拒绝流:第 1025 个并发流会等待服务端的额度,5 秒后以 hysteria2: timed out opening a proxy stream 失败。像上面的客户端示例那样把 max_concurrent_streams 设为服务端的限制,这样的流就会立即失败。其他 Hysteria 专有的取值见 Hysteria 2 页面。

限制 取值 计数对象 达到上限时
max_flows 默认 65,536 TCP 连接,加上每个客户端源地址和端口各一个 UDP 关联 新的 TCP 连接或关联被丢弃。debug 日志:tun: dropping a flow; the flow limit is reached
每个 UDP 源排队的新对端 16 客户端源地址刚刚发往、但其关联尚未接手的对端 发往更多新对端的第一个数据包被丢弃;队列腾出空间后,客户端的重传即可通过

路由环路,即出站自身的流量又被引回设备,会很快占满 max_flows。TUN 页面说明了如何让代理自身的流量不经过该设备。

路由到 wireguard 出站的所有流共用一条隧道和一个驱动任务,但每个流在应用与隧道之间都有自己固定大小的缓冲区。这些缓冲区都不可配置。

缓冲区 取值 单位 达到上限时
TCP socket 缓冲区 每个方向 64 KiB 隧道内的每个 TCP 连接 上行:后续写入留在通道中。下行:远端等待隧道的 TCP 窗口
TCP 通道 发往隧道方向 256 次写入,发往客户端方向 256 次读取,另加最多一次已被驱动取走、但 socket 尚未接收的写入 隧道内的每个 TCP 连接 上行:下一次写入等到远端再次读取。下行:驱动停止读取该 socket,直到客户端读取
UDP 环形缓冲区 每个方向 64 KiB 和 64 个数据报 每个 UDP 关联 发送:遇到环形缓冲区已满的数据报等待下一轮处理。大于整个环形缓冲区的数据报,或发往不可寻址目标(例如端口 0)的数据报,会被丢弃。接收:环形缓冲区已满时到达的数据报被丢弃
UDP 通道 每个方向 256 个数据报,另加发往隧道方向最多一个暂存的数据报 每个 UDP 关联 上行:发送方等待。下行:驱动停止读取环形缓冲区,直到客户端读取

达到上限时,只有停滞的那个流会等待,同一隧道上的其他流照常传输。被丢弃的出站数据报只在 trace 级别记录日志,形如 wireguard: dropping a N-byte datagram to <addr>: <error>。WireGuard 页面的单个慢流一节按顺序介绍了这些缓冲区。

项目 取值
缓存的域名数 8192;缓存满时,无论 TTL 如何都会淘汰条目
DNS 应答中的 TTL(udp、tls、https) 限制在最小 5 秒、最大 3600 秒之间
system 后端的 TTL 60 秒,因为 getaddrinfo 不返回 TTL
失败的查询 不缓存

以上都不可配置。解析器的完整说明见 DNS。

限制 取值 所在端 达到上限时
WebSocket 消息和帧 1 MiB 两端 会话失败
WebSocket early data 16 KiB 两端 入站:以 413 Payload Too Large 响应升级请求。出站:超过 16 KiB 的 ?ed= 值会被截断到上限
gRPC 消息 1 MiB 两端 流失败:grpc message length exceeds the accepted maximum
HTTP/2 并发流 每个连接 256 个 入站 通过 SETTINGS 通告给客户端;遵循规范的客户端会等待空闲的流
HTTP/2 窗口和帧 每个流 4 MiB,每个连接 16 MiB,帧 256 KiB 两端 流量控制;发送方等待

隧道中的载荷远小于这些上限。设置这些上限是为了防止恶意对端让服务器缓冲一条由它任意指定大小的消息。各种承载方式见传输层。

限制 取值 适用范围 达到上限时
HTTP 请求头 64 KiB,最多 128 个头部 http 入站 连接关闭:http head exceeds maximum size,或 malformed http request: too many headers
Trojan UDP 载荷 每个数据包 8192 字节 trojan 入站 关联结束:trojan: oversize payload
完整送达的回复数据报 Trojan、VLESS 和 VMess 为 8192 字节;Hysteria 2 和 TUN 为 4096 字节;SOCKS 为 64 KiB 从出站发回客户端的 UDP 回复 更长的回复会被截断,就像内核把数据 recv 到过短的缓冲区时一样
VMess 时钟偏差 前后各 120 秒 vmess 入站 客户端无法认证;见 VMess
Shadowsocks 2022 时钟偏差 前后各 30 秒 使用 2022- 方法的 shadowsocks 请求被拒绝:shadowsocks-2022: bad timestamp

UDP 能否工作取决于两端:入站必须能中继数据报,每个数据包被路由到的出站也必须能承载数据报。

协议 作为入站 作为出站
socks 支持,在 udp = true(默认)时通过 UDP ASSOCIATE;SOCKS4 不支持 UDP 支持,通过 UDP ASSOCIATE;数据报以普通 UDP 发往服务器的中继端口
http 不支持 不支持
shadowsocks(所有方法) 不支持 不支持
trojan 支持,始终开启;也支持 mux.cool 配合 XUDP 支持,在 Trojan 连接内部
vless 支持,始终开启;也支持 mux.cool 配合 XUDP 支持,在 VLESS 连接内部。该帧格式没有逐包地址,因此每个数据包都发往打开该子链路的那个数据包的目标
vmess 支持,始终开启;也支持 mux.cool 配合 XUDP 支持,在 VMess 连接内部,同样遵循与 VLESS 相同的单目标规则
hysteria2 仅在 udp = true 时支持(默认关闭) 支持,以 QUIC 数据报传输,前提是服务端允许 UDP
tun udp = true(默认)时支持 不是出站
wireguard 不是入站 支持
freedom 不是入站 支持,每个地址族一个套接字
blackhole 不是入站 接受并丢弃
负载均衡器 不是入站 取决于它选中的成员

两端不匹配时的情况:

  • 入站不支持 UDP。 HTTP 或 Shadowsocks 客户端无法请求 UDP。SOCKS5 客户端在 udp = false 的入站上请求 UDP ASSOCIATE 会得到 “command not supported”。udp = false 的 Hysteria 2 服务端会在认证时告知每个客户端它不中继 UDP。
  • 出站不支持 UDP。 路由器并不知道哪些出站能承载数据报。被路由到 http 或 shadowsocks 的数据包会被丢弃,该关联的其他数据包照常传输。debug 日志显示 udp fan-out: opening an outbound failed: http carries no datagrams,其中的 http 也可能是 shadowsocks 或 shadowsocks-2022。请在会把 UDP 发往那里的规则之前放一条 network = "udp" 规则。
  • Hysteria 2 服务端拒绝 UDP。 路由到服务端关闭了 UDP 的 hysteria2 出站的数据包会被丢弃。该出站会记录一条警告,debug 日志显示 udp fan-out: opening an outbound failed: hysteria2: the server does not relay UDP。

规则对每个 UDP 数据包单独匹配;见路由和 UDP 如何到达出站。

配置错误在加载文件或用 --test 检查时出现,前缀为 configuration invalid:。除非另有注明,运行时消息都以 debug 级别记录,因此需要提高 [log].level 才能看到。

消息 原因 解决方法
inbound TAG: max_connections must be at least 1(或 max_circuits) Hysteria 2 的某个限制为 0 删除该键以使用默认值,或设为 1 及以上
inbound TAG: udp_idle_timeout must be between 2 and 600 seconds Hysteria 2 的 udp_idle_timeout 超出范围 使用 2 到 600
inbound TAG: udp_idle_timeout is set but udp is not enabled 在没有 udp = true 的 Hysteria 2 入站上设置了 udp_idle_timeout 添加 udp = true 或删除该键
outbound TAG: max_concurrent_streams must be at least 1 max_concurrent_streams = 0 删除该键或设为 1 及以上
inbound TAG: tun mtu must be at least 1280 TUN 的 mtu 小于 1280 使用 1280 及以上
invalid type: string "30s", expected u64 时长写成了字符串 写成整秒:probe_interval = 30
accept error, backing off 100ms: Too many open files (os error 24)(警告) 进程耗尽了文件描述符 提高打开文件数上限,例如在 systemd 下设置 LimitNOFILE=
dropping inbound connection; live connection limit reached 一个入站上有 65,536 条存活连接 把客户端分散到多个入站或主机上
dropping inbound connection; handshake limit reached 同时有 2,048 条连接处于握手阶段,通常是扫描或洪泛 在防火墙中过滤该来源
inbound handshake timed out after 10s 客户端连上后什么都没发送,或停滞了 10 秒 检查客户端的协议、端口和传输层设置
client did not complete its request in time 客户端开始发送请求,但没有在 10 秒内完成 客户端停滞或非常缓慢,或只发送了部分请求
failed to connect to any address (...) 每个解析出的地址都拒绝连接或在 10 秒后超时 检查目标,或限制 address_family
dns: query timed out DNS 服务器没有在 5 秒内应答 检查 [dns].server 及通往它的路径
hysteria2: timed out opening a proxy stream 服务端 5 秒内没有给出流额度 调低 max_concurrent_streams 以快速失败,或增加第二个出站
hysteria2: connection is at its concurrent-stream limit 出站上已打开 max_concurrent_streams 个流 调高 max_concurrent_streams
hysteria2: refusing a connection; the listener is full 已有 max_connections 个存活的 QUIC 连接 调高 max_connections
hysteria2: refusing a stream; the listener is at its circuit limit 已有 max_circuits 个存活的 circuit 调高 max_circuits
tun: dropping a flow; the flow limit is reached 已有 max_flows 个存活的流,或存在路由环路 修复环路,或调高 max_flows
grpc message length exceeds the accepted maximum 对端发送了超过 1 MiB 的 gRPC 消息 对端不是标准的 gRPC 隧道客户端