👻 XHTTP and HTTP/2
Improvements of XHTTP in Prizrak-Core. The flow governor and the send buffer are based on ideas by Medium1992 from mihomo-proxy-ros; the listener options follow Jolymmiles/Xray-core and Xray-core.
HTTP/2 flow governor (h2-flow)
Section titled “HTTP/2 flow governor (h2-flow)”The client side of an HTTP/2 flow governor for XHTTP in h2 mode. The server sends data only as fast as the client reads it. Without the governor the server fills large HTTP/2 windows (4 MiB per stream in Go) and data nobody reads piles up in memory. The server needs no setup: the governor uses standard HTTP/2 flow control and works with any server.
How it works:
- Window by consumption. The governor shows the server a 64 KiB window, caps the frame size at 16 KiB and measures RTT with its own PING once a second. The server gets credit back only while unread data stays below what the client reads in a couple of round trips.
- Fast start. A new stream starts with a 256 KiB window or the window the connection recently needed. Until the first RTT is measured, the window opens with every credit.
- Path model from
min_rtt. On Linux and Android the governor reads TCP_INFO from the kernel (rtt,min_rtt,rttvar) and holds a download window to at most1.25 × highest read rate × min_rtt. On other systems it uses its PING measurements. - Works with a server-side governor. The same governor exists on the server side in Jolymmiles/Xray-core (the
h2Flowoption). A governed client announces the default 65535 window, which tells the server that a governor sits on the other side too, so the server does not hold back what it sends. The governor can therefore be on the client, on the server or on both: downloads do not stall and throughput stays the same. The Prizrak-Core listener can run the server side too, see Listener below.
Off by default, enabled per proxy with an h2-flow block:
xhttp-opts: h2-flow: enabled: true max-stream-receive-window: 16777216 # receive window per stream, bytes max-connection-receive-window: 33554432 # receive window per connection, bytesenabledturns the governor on.- Receive windows:
- optional, from 65535 bytes to 1 GiB;
- unset, Go’s default windows apply (4 MiB per stream);
- with the governor they are a ceiling and the real window follows reading;
- without it they are fixed windows.
h2-flow-control: truestill works as a shorthand forh2-flow.enabled: true. When both are set,h2-flow.enabledwins.- Downloads through
download-settingsuse the sameh2-flowblock.
Works only in h2 mode; it has no effect for h3 and http/1.1.
To enable h2-flow for Remnawave subscription users, use the Remnawave host mapper; see XHTTP Flow Control on the Remnawave integration page.
HTTP/2 send buffer: 32 KiB
Section titled “HTTP/2 send buffer: 32 KiB”Long-lived HTTP/2 streams keep their send buffer open for the whole connection: XHTTP in stream-one/stream-up modes and gRPC. Its size is reduced from 512 KiB to 32 KiB through the legiz-ru/prizrak-http fork on top of metacubex/http v0.1.8. With many concurrent tunnels this saves noticeable memory at the same speed. Nothing to configure.
GET uploads in packet-up mode
Section titled “GET uploads in packet-up mode”In packet-up mode Prizrak-Core can send its upload requests with GET, as Xray does with "uplinkHTTPMethod": "GET". The data travels in the request body, in a header or in a cookie, chosen by uplink-data-placement. The Prizrak-Core listener accepts such requests from Xray clients as well.
xhttp-opts: path: /x mode: packet-up uplink-http-method: GET uplink-data-placement: header # body, header or cookieGETworks inpacket-upmode only. Instream-oneandstream-upthe config fails to load with an error.- The method is case-insensitive:
getandGETare the same. uplink-data-keydefaults as in Xray:X-Datafor header andx_datafor cookie. Set it only when the server uses another name.- For header and cookie one upload is 4 KiB when
sc-max-each-post-bytesis unset: the Xray server accepts headers up to 8 KiB. The Xray client defaults to 1 MB there and needs that lowered by hand.
Xray option mapping:
Xray (xhttpSettings) | Prizrak-Core (xhttp-opts) |
|---|---|
"mode": "packet-up" | mode: packet-up |
"uplinkHTTPMethod": "GET" | uplink-http-method: GET |
"uplinkDataPlacement": "header" | uplink-data-placement: header |
"uplinkDataKey": "X-Data" | uplink-data-key: X-Data |
"scMaxEachPostBytes": "4000" | sc-max-each-post-bytes: 4000 |
One HTTP/2 connection at a time
Section titled “One HTTP/2 connection at a time”The XHTTP client in h2 mode dials one HTTP/2 connection to a server at a time. Before, requests that started together, as when a page opens, each opened a connection of their own: 16 requests on a fresh connection made 13 connections, and 16 while a TLS handshake hung (as when a censor drops the flow after the ClientHello). A burst of handshakes is a pattern DPI looks for, and it went past reuse-settings.max-connections. Now such requests wait for one dial and share its connection; another connection opens only when the server’s stream limit is reached. Nothing to configure. It is the same fix as -tags http2legacy in Xray builds (XTLS/Xray-core#6797).
In packet-up mode an upload is also sent again on a new connection when the server or a CDN retires the old one with GOAWAY; before, the upload failed and the connection dropped (as Xray since XTLS/Xray-core#6632).
OPTIONS uploads
Section titled “OPTIONS uploads”uplink-http-method: OPTIONS works with the Prizrak-Core listener as with Xray since v26.10.31 (XTLS/Xray-core#7290): an OPTIONS request with Access-Control-Request-Method is a CORS preflight, one without it is an upload. Before, the listener answered every OPTIONS as a preflight and dropped the data. The client sends any method it is given, OPTIONS included; the server must be Xray v26.10.31 or newer, or Prizrak-Core after v1.19.33-r8.
Listener: governor, HTTP/2 settings and Mux.Cool KeepAlive
Section titled “Listener: governor, HTTP/2 settings and Mux.Cool KeepAlive”The VLESS listener’s xhttp-config takes three server options, as in Jolymmiles/Xray-core and Xray-core:
listeners: - name: vless-xhttp type: vless # ... xhttp-config: path: /x mux-keep-alive-secs: "20-25" # poke an idle packet-up / stream-up downlink mux-keep-alive-bytes: "100-500" h2-flow: enabled: true # max-stream-receive-window: 6291456 # max-connection-receive-window: 6291456 # max-connection-send-window: 4194304 # -1: no limit # queue-cuts: true h2-params: max-concurrent-streams: 256 # max-read-frame-size: 16384 # send-ping-timeout: 30sh2-flow: the server side of the flow governor. Off by default; HTTP/2 only (TLS, REALITY and h2c).- Upload windows
max-stream-receive-windowandmax-connection-receive-window: from 65535 bytes to 1 GiB; unset, 6 MiB with the governor and Go’s 1 MiB without. max-connection-send-windowbounds what one connection’s client holds of the server’s data, unread or as open window: 4 MiB by default,-1lifts the limit. A client with a governor of its own is not held to it.queue-cuts(on by default) keeps a download at its rate beside other traffic on the same path;falseturns the cuts off.
- Upload windows
h2-params: HTTP/2 settings of the server, ash2Paramsin Xray (XTLS/Xray-core#7295):max-concurrent-streams,max-read-frame-size,max-receive-buffer-per-connection,max-receive-buffer-per-stream,max-decoder-header-table-size,max-encoder-header-table-size,send-ping-timeout,ping-timeout,write-byte-timeout(durations like30sor a number of seconds),strict-max-concurrent-requests,permit-prohibited-cipher-suites. Unset fields keep Go’s defaults. A window set inh2-flowwins over the one here. Withh2-flowon,max-concurrent-streamsis at most 4096.mux-keep-alive-secsandmux-keep-alive-bytes. When a client sends mux.cool over XHTTP and its packet-up or stream-up download carries nothing for that many seconds, the server sends a KeepAlive frame with random padding of that size (at most 1024 bytes). Clients discard it, and CDNs and reverse proxies do not cut the download as idle. Off by default;stream-oneis never poked.- A wrong value fails the listener when the config loads, before it opens a port.
Option mapping:
Xray (xhttpSettings) | Prizrak-Core (xhttp-config) |
|---|---|
"h2Flow": {"enabled": true} | h2-flow: {enabled: true} |
"h2Flow": {"maxConnectionSendWindow": -1} | h2-flow: {max-connection-send-window: -1} |
"h2Flow": {"queueCuts": false} | h2-flow: {queue-cuts: false} |
"h2Params": {"MaxConcurrentStreams": 256} | h2-params: {max-concurrent-streams: 256} |
"muxKeepAliveSecs": "20-25" | mux-keep-alive-secs: "20-25" |
"muxKeepAliveBytes": "100-500" | mux-keep-alive-bytes: "100-500" |