docs(websocket): record WS-13 no-keepalive decision + progress semantics (WS-13)
Decides the WS-13 legitimate-silence question as option (b): 60s of no chunk progress is an intentional eviction line even for silent subscriptions; no WS ping/pong keepalive is added because a keepalive can only rescue app-silence by re-arming the deadline, which reopens the dribble hole the knob exists to seal. Documented in the byte_adapter module doc, on DEFAULT_WS_IDLE_TIMEOUT, on the unchanged HttpAdapter::with_ws_idle_timeout knob, and in websocket.md (new 'Idle-read timeout' section, including the FWD-15 SSE-keepalive layering note). Deployment posture for long-lived silent sessions: with_ws_idle_timeout(None) + WsSessions abort + write-side caps.
This commit is contained in:
@@ -32,6 +32,32 @@
|
||||
//! Text WS messages are rejected with a protocol-level close (code
|
||||
//! 1002); all frames are binary (websocket.md §Framing).
|
||||
//!
|
||||
//! Idle-read timeout (WS-01, WS-13 semantics): the knob
|
||||
//! (`DEFAULT_WS_IDLE_TIMEOUT`, deployment-adjustable via
|
||||
//! `HttpAdapter::with_ws_idle_timeout`, disable via `None`) evicts a
|
||||
//! connection whose inbound stream produces **no completed chunk for
|
||||
//! the whole window** — the deadline resets on demux progress (bytes
|
||||
//! forwarded into `read_tx` that complete 8-byte-header-framed chunks),
|
||||
//! never on WS message arrival, so a forever-dribble inside a declared
|
||||
//! chunk hits the deadline even though messages keep arriving, while a
|
||||
//! peer delivering complete chunks — however slowly per-message —
|
||||
//! re-arms the window with each one.
|
||||
//!
|
||||
//! Legitimate silence (WS-13 decision, recorded — option (b)): there
|
||||
//! is deliberately **no WS ping/pong keepalive**. A keepalive can only
|
||||
//! rescue app-silence by re-arming the deadline, which would reopen the
|
||||
//! dribble hole it exists to seal (pong = traffic from the attacker's
|
||||
//! point of view); instead, 60 s of *no chunk progress* is an
|
||||
//! intentional eviction line even for a silent subscription — a
|
||||
//! long-lived quiet subscription that must survive past the window
|
||||
//! (with server-side pushes; see the keep-alive discussion in
|
||||
//! `websocket.md`) is exactly the deployment that dials
|
||||
//! `with_ws_idle_timeout(None)` and leans on the other bounds
|
||||
//! (`WsSessions::abort` eviction, the write-side caps). Read eviction
|
||||
//! closes with 1001 (Going Away) — a normal connection end from the
|
||||
//! demux's point of view (EOF → channels cleared, pendings failed),
|
||||
//! not a protocol error.
|
||||
//!
|
||||
//! Close mapping: WS close (either side) → read EOF → the demux clears
|
||||
//! all channels (REQ-CH-02) and the dispatch loop fails outstanding
|
||||
//! pendings. `AsyncWrite::shutdown` closes the WS sink after the queued
|
||||
@@ -107,20 +133,27 @@ pub const WS_PROTOCOL_ERROR: u16 = 1002;
|
||||
/// violation it cannot recover from (WS-04/HY-09, WS-05).
|
||||
pub const WS_INTERNAL_ERROR: u16 = 1011;
|
||||
|
||||
/// Idle-read close code (WS-01): a read side that stays silent past the
|
||||
/// configured idle timeout is closed with 1001 (Going Away) — a normal
|
||||
/// connection end from the demux's point of view (EOF → channels
|
||||
/// cleared, pendings failed), not a protocol error.
|
||||
/// Idle-read close code (WS-01/WS-13): a read side that produces no
|
||||
/// demux progress (no completed inbound chunk) past the configured
|
||||
/// window is closed with 1001 (Going Away) — a normal connection end
|
||||
/// from the demux's point of view (EOF → channels cleared, pendings
|
||||
/// failed), not a protocol error.
|
||||
pub const WS_GOING_AWAY: u16 = 1001;
|
||||
|
||||
/// Default idle-read timeout for the WS pumps (WS-01): a peer whose
|
||||
/// inbound byte stream stops producing **complete chunks** cannot
|
||||
/// park the single demux loop longer than this (WS-13 progress
|
||||
/// semantics — the deadline resets when a complete chunk's bytes are
|
||||
/// forwarded into `read_tx`, not when WS messages arrive, so a
|
||||
/// forever-dribble inside a declared chunk still hits it). Zero-wait
|
||||
/// is spelled `None`; this duration is the deployment default
|
||||
/// (`HttpAdapter::with_ws_idle_timeout`).
|
||||
/// Default idle-read timeout for the WS pumps (WS-01, WS-13): a
|
||||
/// connection whose inbound stream completes **no chunk** within this
|
||||
/// window is evicted — the deadline resets on demux progress (complete
|
||||
/// chunks forwarded into `read_tx`), not on WS message arrival, so a
|
||||
/// forever-dribble inside a declared chunk still hits it.
|
||||
///
|
||||
/// This is an intentional no-progress eviction line, *not* a
|
||||
/// transport-idle bound: there is no WS ping/pong keepalive, and
|
||||
/// app-silence that outlasts the window (a quiet subscription) is
|
||||
/// evicted with 1001 by design — see the module doc's "Legitimate
|
||||
/// silence" decision. A deployment running long-lived silent
|
||||
/// subscriptions disables the knob with
|
||||
/// `HttpAdapter::with_ws_idle_timeout(None)` (`None`, not zero — zero
|
||||
/// is not a meaningful window).
|
||||
pub const DEFAULT_WS_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Observes the inbound byte stream for chunk framing (WS-13): counts
|
||||
|
||||
Reference in New Issue
Block a user