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§
- Frame
Counter - 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.
- WsPolice
Io - 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§
- Frame
Violation - 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
Upgradeheader is a comma list; the token is case-insensitive per RFC 7230). - origin_
allowed - The origin gate:
truewhen the handshake may proceed. A non-empty allowlist admits only exact matches; a missingOriginis denied under a non-empty allowlist (fail closed).