Under construction

This documentation is still in progress. Some information may be incomplete or out of date. Please check back later.

Docs/Protocol stacks

Internals

Protocol stacks

httpjet speaks HTTP/1.1, a from-scratch native HTTP/2 stack, and HTTP/3 over QUIC through one unconditional io_uring / monoio transport. TCP and QUIC converge behind one rustls configuration, every protocol hands requests to the same pipeline, and file bodies leave over sendfile(2) where the kernel allows it.

Overview

All serving lives in the binary's uring/ module. HTTP/1.1 is a custom monoio + httparse codec; HTTP/2 and cleartext h2c use the native hj-h2 stack through its monoio serve path; HTTP/3 is uring/h3.rs, a quinn-proto QUIC loop over io_uring UDP with quinn-udp socket metadata (GSO/GRO). Protocol selection happens at the connection boundary: ALPN over TLS, a non-destructive preface sniff in cleartext, and QUIC on the UDP listener.

multishot acceptlistener hasproxyProtocol=1secure listenerplain listenerheader valid (else close)rustls (TLS 1.2 / 1.3,SNI, optional clientcert)http/1.1h224-byte h2c prefaceanything elsekeep-alive ≤maxKeepAliveReqstreams multiplexed onone taskConnection close ·timeout · errorGOAWAY drain · timeout

Accepted

ProxyHeader

Handshake

Sniff

ALPN

H1

H2

Closed

A TCP connection's life. PROXY-protocol stripping (if the listener opts in) happens before the TLS handshake or the cleartext sniff, so every later stage sees the proxied client as the peer. ALPN picks the TLS stack; the 24-byte h2 preface picks the cleartext one. H2 connections drain with GOAWAY on shutdown.
diagram source (mermaid)
stateDiagram-v2
  direction LR
  [*] --> Accepted: multishot accept
  Accepted --> ProxyHeader: listener has proxyProtocol=1
  Accepted --> Handshake: secure listener
  Accepted --> Sniff: plain listener
  ProxyHeader --> Handshake: header valid (else close)
  ProxyHeader --> Sniff
  Handshake --> ALPN: rustls (TLS 1.2 / 1.3, SNI, optional client cert)
  ALPN --> H1: http/1.1
  ALPN --> H2: h2
  Sniff --> H2: 24-byte h2c preface
  Sniff --> H1: anything else
  H1 --> H1: keep-alive ≤ maxKeepAliveReq
  H2 --> H2: streams multiplexed on one task
  H1 --> Closed: Connection close · timeout · error
  H2 --> Closed: GOAWAY drain · timeout
  Closed --> [*]
HTTP/1.1 · custom monoio codec HTTP/2 · native hj-h2 h2c · prior-knowledge sniff HTTP/3 · quinn-proto on io_uring HPACK · RFC 7541, zero-alloc TLS · rustls / aws-lc-rs · optional kTLS

Connection front doors

Two per-listener features sit before protocol detection. Both are httpjet-native — LiteSpeed has no listener-level equivalent — and both change who httpjet thinks the client is.

PROXY protocol v2 / v1

A listener with <proxyProtocol>1</proxyProtocol> requires every connection to open with a PROXY header. The header is consumed and the claimed client address installed before the TLS handshake on secure listeners and before the H1/h2c discriminator on plain ones. Missing header → the connection is closed (fail-closed both ways). Since 2026-09-16 a header that claims a loopback or unspecified source is refused at the parser: those addresses feed the mTLS exemption and the loopback purge gate, so accepting them would let a proxy hop forge local trust.

!
The listener opt-in is the trust decision

Unlike OpenLiteSpeed's server-level trusted-peer ACL, httpjet binds PROXY trust to the listener (nginx/HAProxy semantics). Enabling it on an internet-facing bind lets anyone forge their client address; check warns on wildcard or public binds. Point such a listener only at your load balancer or a local hop.

HTTP over Unix sockets

A listener whose address is unix:/path/sock (or uds:), or the --http-uds flag, serves plain HTTP over a Unix-domain socket: H1 and h2c prior knowledge, one accept core (SO_REUSEPORT does not exist for AF_UNIX), socket mode 0600 group-writable, socket-activated fds adopted by path. A stale socket file is unlinked only after a connect(2) proves it dead, and cleanup is bound to the owned directory and socket identity. Peers have no address: ACLs, the per-IP throttle and REMOTE_ADDR see a fabricated loopback value and the access log renders unix:. TLS over UDS is unsupported and linted.

HTTP/1.1 & h2c detection

The H1 codec parses with httparse, enforces strict framing (single Content-Length, conflicting-length rejection, bounded incomplete reads), supports keep-alive and pipelining, honors maxKeepAliveReq (0 = unlimited), decodes chunked bodies, and streams large responses from the bridge without buffering. Since the 2026-10-06 review it also enforces the RFC 9112 request-target and Host rules at intake: only origin-form targets (plus * for OPTIONS) and http/https absolute-form (reduced to origin-form, its authority replacing Host); CONNECT answers 405, authority-form and a raw # answer 400. HTTP/1.1 requires Host, and any Host must be UTF-8 and pass the same authority validation the h2/h3 :authority rule uses (HTTP/1.0 may omit it). A header line terminated by a bare LF is rejected 400 before the head is materialized.

Cleartext HTTP/2 by prior knowledge has no upgrade handshake — the client simply sends the connection preface. httpjet detects it non-destructively: the bytes already read are handed to whichever stack wins, so h1 and h2 both parse the connection as if no router sat in front.

uring · ALPN + cleartext routing
// over TLS: ALPN decides the TCP stack
match alpn {
    b"h2"       => hj_h2::server::serve_local_with_prefix(io, prefix, service, cfg).await,
    _           => handle_h1_bridged(io, prefix, service).await,   // http/1.1 or none
}

// cleartext: sniff the 24-byte preface, pass the observed bytes along
match sniff_cleartext(&mut stream).await? {
    Proto::Http2(prefix) => hj_h2::server::serve_local_with_prefix(stream, prefix, service, cfg).await,
    Proto::Http1(prefix) => handle_h1_bridged(stream, prefix, service).await,
}

Beyond framing, the H1 path implements the newer semantics end to end:

Native HTTP/2

hj-h2 is httpjet's own HTTP/2 server. Every h2 connection — ALPN h2 over TLS and cleartext h2c — lands here through the monoio local serve path. The design targets the CDN→origin shape: few long-lived connections, many concurrent streams, so it optimizes one connection's multiplexing rather than spreading work across many.

TLS connALPN h2 / h2c ONE connection task FuturesUnordered⟨stream⟩ stream 1 · handler stream 2 · handler … stream N HPACK enc / dec send flow control · deadlines one vectored writev / wake wirezero-copy Bytes bridge dispatch → shared pipeline → monoio writes
One connection task multiplexes every stream and emits a single vectored write per wake. Handlers cross the bridge into the shared pipeline; framing stays on the monoio core.
⎁
Measured

The native stack passes 146 of 146 h2spec 2.6.0 cases over TLS (three consecutive runs is the gate). h2c is informative-only: generic §3.5/2 is structurally unsatisfiable on a port that also sniffs HTTP/1.1, so it records 145/146 with that one documented exception.

scripts/h2spec_check.sh · scripts/ci-h2spec.py

HPACK

Header compression is RFC 7541 HPACK, implemented as the hpack submodule of hj-h2 (once a standalone crate, folded in so the codec and the connection state evolve together). It is zero-allocation — fields are decoded and encoded by slicing frame bytes in place — and pure safe Rust with #![forbid(unsafe_op_in_unsafe_fn)]. Both the decoder and the round-trip are fuzz targets (Conformance).

HTTP/3 & QUIC

HTTP/3 is uring/h3.rs: quinn-proto for QUIC state, quinn-udp for socket metadata, UDP driven from monoio/io_uring, QPACK encode/decode, and the same bridge into the pipeline. Small, cached and sub-threshold bodies are buffered and sent whole; large, SSE and proxy bodies stream — HEADERS goes out immediately and DATA follows as the backend produces it, with backpressured writeback. Retained response memory is bounded per connection (2026-09-06). The process-wide connection budget (shared with TCP accepts) is charged at Connected — after address validation — so spoofed Initials cannot hold budget for the idle timeout; until then a connection costs only one of 256 per-core handshake slots, and once 64 connections on a core are handshaking an unvalidated Initial receives a stateless Retry (token-bearing Initials are still accepted). A full budget closes with H3_EXCESSIVE_LOAD; handshakes are closed after 10 s.

Discovery is via Alt-Svc on TLS responses (the last response transform), so an h2 client learns it can upgrade to QUIC. Connection migration is disabled because listener steering does not yet preserve ownership across an address change. Each connection pins its trust generation and mTLS requirement at acceptance; a trust reload replaces quinn's server configuration only for future handshakes while the bound UDP workers and established connection IDs survive.

!
Publish UDP too

The same numeric HTTPS port must be reachable over TCP (h1/h2) and UDP (h3). Publishing 8443/tcp alone means no HTTP/3, and the Alt-Svc header will advertise a port clients cannot reach.

File egress: sendfile & kTLS

An uncached Body::File keeps its pinned inode and byte range across the tokio→monoio bridge. On plain TCP HTTP/1 it is sent with bounded nonblocking sendfile(2); unsupported descriptor/filesystem combinations resume at the exact offset with positional io_uring reads, and short files or client aborts close the connection rather than completing an incorrect Content-Length. A stalled sendfile backs off exponentially and gives up after 60 s of zero progress.

A --features ktls build extends this to TLS 1.3 H1: after the rustls handshake the socket is upgraded to kernel TLS, so the same sendfile writer feeds ciphertext without a userspace AEAD copy. A peer KeyUpdate queued during a transfer is applied before the next response writes, fencing TX rekey from sendfile. Policy is --ktls=auto|on|off: auto (default in feature builds) resolves the IPv4 default-route interface, reads its driver and features via ethtool ioctls, refuses known virtual NICs, and enables kTLS only when tls-hw-tx-offload is active — otherwise it logs one reason and stays on rustls. TLS 1.2, H2, H3 and UDS keep their userspace paths.

⎁
Measured

Plaintext sendfile: 5,000 × 1 MiB at c=50 → 4,852 req/s, 10.08 ms mean, zero failures. Software kTLS on a virtual GVE NIC (offload off [fixed]): 2,999.7 req/s off vs 2,944.9 on — the expected-negative result that is why auto refuses virtual NICs.

loopback · 2026-09-09 · bench-results/httpjet-sendfile-*.csv, httpjet-ktls-sendfile-*.csv

TLS in one paragraph

hj-tls builds one rustls::ServerConfig for TCP and QUIC with aws-lc-rs as the crypto provider. SNI maps directly to configured chains (no cert-name-match, like OLS), with dual ECDSA+RSA chains per name; client verification is optional mTLS against the listener CA with CRL enforcement; TLS 1.3 offers exactly TLS_AES_128_GCM_SHA256; resumption uses single-use tickets with 0-RTT off. Certificates, ACME and OCSP have their own page: TLS, ACME & OCSP.

httpjet · documentation · synced to the source tree on 2026-10-13 (v0.0.5).
Source: github.com/faratech/httpjet · Home: httpjet.net · Found an error? Open an issue on the repository.