Module websocket

Module websocket 

Source
Expand description

WebSocket policy (DW-039 + DP-05 #253): the knobs that turn the generic 101 tunnel into a managed one for browser-style WebSocket traffic — origin allowlisting at the handshake, post-upgrade frame-rate policing, per-frame size limits, idle-timeout reaping, and RFC 6455 protocol-error enforcement on the tunnel.

§Zero new dependencies, by construction

The module hand-rolls exactly as much of RFC 6455 as policing needs and not one byte more. The frame scanner reads HEADERS only (2..=14 bytes: opcode + mask bit + 7/16/64-bit lengths) — it never unmasks, never inspects payload bytes. The origin check is a string comparison against config values. The close frames the policer sends (opcode 8, status 1008/1009/1002) are four fixed bytes each. Everything else — the handshake itself, the upgraded byte pipe — stays the generic tunnel’s job (proxy::tunnel / proxy::idle_tunnel); a non-WebSocket upgrade (any other Upgrade token) never enters this module.

§The origin gate

Applied in the proxy action when the request offers a websocket upgrade: a non-empty routes[].websocket.origins list admits ONLY exact (case-sensitive) matches. A MISSING Origin header is denied — browsers always send one on a WebSocket handshake, so an originless handshake is a non-browser client the operator did not name; fail closed. The check denies BEFORE any upstream contact (no dial, no breaker observation) with the gateway’s standard envelope, code websocket_origin_denied.

§The frame-rate policer

WsPoliceIo wraps the UPGRADED CLIENT side of the tunnel. Every byte the client sends passes through transparently while a scanner tracks frame boundaries and counts DATA frames (text 0x1, binary 0x2, continuation 0x0; ping/pong/close are free — they are the protocol’s own housekeeping, and a ping flood is bounded by the connection itself). The allowance is a token bucket: rate tokens per second, capacity rate (a one-second burst). A data frame with no token left trips the policer: a close frame with status 1008 (policy violation) is queued for the CLIENT direction (written ahead of any other pending bytes), the client read side returns EOF (the tunnel propagates shutdown), and the connection ends. Policing is one-directional by design: it protects UPSTREAMS from abusive clients.

Fragmented messages count per frame — a client fragmenting every message gets policed harder, not softer; the conservative direction is the safe one. Extended (16/64-bit) lengths are data frames by construction: RFC 6455 caps CONTROL frames at 125 payload bytes, so only data frames ever carry them.

§DP-05 (#253): frame size, protocol errors, and idle timeout

The frame scanner was extended to enforce three additional policies, all closing the connection with the appropriate code:

  • max_frame_size_bytes: a DATA frame whose payload length exceeds the configured cap is closed with status 1009 (message too big). The check runs at the header scan — both the 7-bit short length and the 16/64-bit extended length — before the payload is consumed.
  • Reserved opcodes: RFC 6455 reserves 0x3-0x7 (data) and 0xB-0xF (control). A frame using a reserved opcode is closed with status 1002 (protocol error).
  • Control-frame length: control frames with extended lengths (len7 >= 126) violate RFC 6455 §5.5 (control payloads must be <= 125 bytes) and are closed with status 1002.

The idle timeout (idle_timeout_s) is enforced at the tunnel level by proxy::idle_tunnel, which wraps the bidirectional copy in a resettable tokio::time::sleep. Each successful read or write in either direction resets the timer; if it fires, both sides are shut down.

Structs§

FrameCounter
The frame-boundary scanner: fed the exact byte stream the tunnel forwards client-to-upstream, counts data frames. Bytes are ANALYZED, never modified — the tunnel stays byte-exact.
WsPoliceIo
The policing wrapper around the UPGRADED CLIENT side of the tunnel (DW-039, DP-05 #253). Reads pass through byte-exact while the frame scanner counts; on the first unfunded data frame a 1008 close frame is queued for the write path (ahead of any other bytes) and the read side returns EOF — the tunnel then shuts both directions down. Bytes are never modified in either direction.

Enums§

FrameViolation
The kind of protocol violation the frame scanner detected (DP-05, #253). Stored in the shared violation flag so the tunnel spawner records the right metric and close code.
Handshake
The handshake gate’s outcome for one upgrade request.

Functions§

count_data_frames
Data frames counted for a byte stream fed in one chunk (tests and diagnostics).
flag_to_metric
Decode the shared violation flag into the metric label the tunnel spawner records (DP-05, #253). Returns an empty string when no violation occurred.
handshake_verdict
One policy decision at the handshake (DW-039): the proxy calls this before forwarding an upgrade request.
offers_websocket
Whether a request offering upgrades asks for WebSocket (the Upgrade header is a comma list; the token is case-insensitive per RFC 7230).
origin_allowed
The origin gate: true when the handshake may proceed. A non-empty allowlist admits only exact matches; a missing Origin is denied under a non-empty allowlist (fail closed).