Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/klzgrad/naiveproxy/llms.txt

Use this file to discover all available pages before exploring further.

NaïveProxy applies a padding protocol on top of HTTP/2 CONNECT tunnels to mitigate length-based traffic analysis. The protocol is intentionally lightweight and improvised rather than cryptographically elaborate — the design philosophy holds that a large number of simple, distinct circumvention protocol designs creates a better logistical barrier against censorship research than a small number of sophisticated ones. What follows is an informal specification of the protocol as implemented.

Design Philosophy

The padding protocol opts for low overhead and easier implementation. The authors believe that the proliferation of expendable, improvised circumvention protocol designs is a better logistical impediment to censorship research than a small number of sophisticated designs. Simplicity also reduces implementation surface area and makes the protocol easier to audit and port.

Proxy Payload Padding

NaïveProxy proxies bidirectional streams through HTTP/2 (or HTTP/3) CONNECT tunnels. Within each such stream, the first kFirstPaddings = 8 reads and writes after the stream is established are padded. The padded format is:
struct PaddedData {
  uint8_t original_data_size_high;  // original_data_size / 256
  uint8_t original_data_size_low;  // original_data_size % 256
  uint8_t padding_size;
  uint8_t original_data[original_data_size];
  uint8_t zeros[padding_size];
};
  • padding_size is a random integer uniformly distributed in [0, kMaxPaddingSize] where kMaxPaddingSize = 255.
  • original_data_size cannot exceed 65535. If the payload is larger, it must be split into multiple reads or writes, each wrapped in its own PaddedData.
  • Reads and writes after the first 8 are transmitted without padding to avoid ongoing performance overhead. Later packet lengths are generally considered less informative for traffic analysis.

Why 8 padded exchanges?

The value 8 was chosen to cover the complete initial handshake sequences for both sides of the connection, flattening the packet length distribution spikes that arise from predictable protocol preambles.
  1. TLS ClientHello
  2. TLS ChangeCipherSpec + Finished
  3. H2 Magic + SETTINGS + WINDOW_UPDATE
  4. H2 HEADERS GET
  5. H2 SETTINGS ACK
  1. TLS ServerHello + ChangeCipherSpec + …
  2. TLS Certificate + …
  3. H2 SETTINGS
  4. H2 WINDOW_UPDATE
  5. H2 SETTINGS ACK
  6. H2 HEADERS 200 OK
Both sequences fit within 8 exchanges, so kFirstPaddings = 8 ensures that all predictable initial packets carry random padding before any unpadded traffic appears.

H2 RST_STREAM Frame Padding

In practice, NaïveProxy tends to send more RST_STREAM frames per session than a regular browser would. An unusually high RST_STREAM rate is a detectable behavioral signal. To mitigate this, an END_STREAM DATA frame padded to a total length uniformly distributed in [48, 72] bytes is prepended to each RST_STREAM frame, making the pair resemble a HEADERS frame from the outside. The server often replies to this DATA frame with a WINDOW_UPDATE, because padding bytes are counted against HTTP/2 flow control.
Whether the server’s WINDOW_UPDATE reply itself constitutes a new detectable pattern is an open question acknowledged in the original specification.

H2 HEADERS Frame Padding

CONNECT request and response HEADERS frames are typically shorter than regular browser HEADERS frames and appear at lower frequency, making them identifiable by length alone. To normalize their length, a padding header is added to both request and response HEADERS frames. The padding value is filled with pseudo-random symbols chosen specifically to avoid Huffman coding (which would reduce length) and to avoid being indexed in the HPACK dynamic table (which would affect future frames).
FramePadding length distribution
CONNECT requestUniform random in [16, 32] bytes
CONNECT responseUniform random in [30, 62] bytes

Opt-In Negotiation

The padding protocol is opt-in and fully interoperable in both directions:
  • A NaïveProxy client connecting to a standard HTTP/2 proxy that does not support padding will work correctly — padding is simply not activated.
  • A NaïveProxy server receiving a connection from a standard HTTP/2 client (such as a regular Chrome browser) will work correctly — it will not expect or require padding.
Padding is activated only when both sides signal capability:
  1. The client includes the padding header in its CONNECT request.
  2. The server includes the padding header in its CONNECT response.
  3. Only if both headers are present does either side apply the padding protocol for subsequent payload.
The first CONNECT request to a server cannot use Fast Open (i.e., cannot send payload before receiving the response). At this point the server’s padding capability is not yet known — the client has not yet seen a response with a padding header — so it is impossible to know whether the Fast Open payload should be padded or unpadded.

Build docs developers (and LLMs) love