WebSocket
Complete example: wss_vmess.yaml. Adapt addresses and secrets before deployment.
The field sketches below contain placeholders, not runnable values. Use a complete example when configuring a deployment.
Server
Section titled “Server”protocol: type: websocket # Aliases: ws targets: - matching_path: string? # Optional path filter (e.g., "/ws") matching_headers: # Optional header filters X-Custom-Header: "value" max_early_data: 0 # 0: automatic standard-header reception, up to 2048 bytes early_data_header_name: string? # With positive max: empty/omitted selects path suffix protocol: ServerProxyConfig ping_type: ping-frame # disabled | ping-frame | empty-frame override_rules: [RuleConfig]WebSocket early data carries an initial byte prefix in the HTTP Upgrade request,
not in a WebSocket frame and not as TLS 0-RTT. Clients send it only when explicitly
enabled. Servers with omitted or zero max_early_data automatically accept it
from Sec-WebSocket-Protocol, up to 2048 decoded bytes. A positive server limit
selects explicit mode and only the configured carrier is read; there is no
automatic fallback to the standard header. To change the automatic cap, set both
a positive max_early_data and early_data_header_name: Sec-WebSocket-Protocol.
A target with a static Sec-WebSocket-Protocol filter in matching_headers
suppresses automatic reception: its token remains a selector, with no prefix
or subprotocol echo. Otherwise an unclaimed nonempty subprotocol token is treated
as early data, not ordinary subprotocol negotiation. A valid base64url token
such as chat injects decoded bytes; an invalid token is rejected. There is no
general server-side off switch. Missing or uniquely empty headers remain plain
WebSocket upgrades; duplicated carrier headers are rejected in automatic mode,
even when the final value is empty.
Support is not negotiated: a successful upgrade, including an absent subprotocol
echo, does not prove the server accepted the prefix. Automatic reception prevents
the standard-header/default-server mismatch, but mismatched custom headers or
path suffixes reaching a target without matching_path can still silently lose
the prefix, particularly with raw port forwarding. Explicit path/custom-header
targets also do not read the standard header. Configure matching carriers;
no replay can recover a mismatched prefix safely.
Explicit targets accept at most max_early_data decoded bytes.
Malformed, noncanonical or oversized base64url is rejected before the upgrade.
Missing or empty early data is allowed. Decoded bytes precede all framed payload.
With early_data_header_name: Sec-WebSocket-Protocol, Shoes interoperates with
Xray’s /ws?ed=2048 client configuration and sing-box’s named-header mode. Shoes
uses explicit fields; do not put ?ed= in its matching_path. Other nonreserved
HTTP header names are supported for sing-box peers. Names are case-insensitive;
an explicit carrier cannot also appear in matching_headers.
Use sing-box 1.14.3 or newer for client-side early data: older clients can discard
the first response bytes when they arrive together with the HTTP upgrade.
With a positive limit and no header name, encoded bytes are appended directly to
matching_path, as in sing-box’s path mode. This requires an explicit, query-free
server path. Requests matching multiple targets are rejected if any candidate
has a positive limit or the first matching target would automatically receive
data (including a duplicated carrier). Otherwise legacy first-match routing
remains, including when the first target has a static subprotocol selector.
A generic automatic target before a matching static-selector target is ambiguous
for a nonempty token; target order is not changed to prefer selectors. Use
disjoint paths/header filters rather than overlapping prefix paths or catch-all
targets on the same listener.
Limits must fit the HTTP budget: each request/header line is less than
4096 bytes, the header block at most 32768 bytes, and fewer than 40 nonempty
lines. Configuration validation includes base64 expansion and rejects limits
that cannot fit. The aggregate 32768-byte cap also applies to plain WebSocket
upgrades. Clients reject malformed status codes, missing or invalid
Upgrade: websocket and Connection: upgrade response tokens, and duplicate
accept/subprotocol response headers in both modes. Early data is visible to HTTP
intermediaries and their logs; use TLS when confidentiality is required.
Client
Section titled “Client”protocol: type: websocket matching_path: string? matching_headers: header_name: string max_early_data: 0 # Decoded-byte limit; 0 keeps eager upgrades early_data_header_name: string? # Empty/omitted: path suffix ping_type: ping-frame # disabled | ping-frame | empty-frame protocol: ClientProxyConfigExample for Xray or sing-box protocol-header mode:
protocol: type: websocket matching_path: /ws matching_headers: Host: proxy.example.com max_early_data: 2048 early_data_header_name: Sec-WebSocket-Protocol protocol: type: vless user_id: "11111111-1111-4111-8111-111111111111"Early-enabled clients require an explicit matching_headers.Host identifying
the proxy, not the tunneled destination. Omit early_data_header_name for
sing-box path-suffix mode; that mode does not support query strings.
Sending remains off by default. Default Shoes servers accept standard-header
early data up to 2048 bytes without additional server configuration; path and
custom-header carriers require explicit server configuration.
The first nonempty write contributes up to the configured limit; remaining bytes use ordinary WebSocket frames. For vectored writes, only the first nonempty slice contributes to the early prefix. Inner protocol headers normally make up this first write, not the application’s first payload. A read, flush, shutdown or enabled ping before the first write performs an ordinary upgrade instead. Empty reads/writes do not initiate it. Upgrade failures are terminal and never replay the accepted prefix. Deferred upgrades retain the 60-second setup timeout.