# MicroProxy
> A dependency-free Java 21 HTTP/1, HTTP/2, HTTP/3/QUIC, SOCKS and MITM proxy on virtual threads, the successor to LittleProxy.
# MicroProxy
MicroProxy is a dependency-free Java 21 proxy for HTTP/1, HTTP/2, HTTP/3 over QUIC, SOCKS and TLS
interception (MITM), built on virtual threads and blocking I/O. This page says what it is, lists
every feature with a link to its guide, and explains the design; start with [Getting
started](https://mahmoudimus.github.io/MicroProxy/getting-started/install/) to run it.
MicroProxy is an HTTP/HTTPS proxy for Java that can intercept and rewrite traffic. It is a
port of [LittleProxy](https://github.com/LittleProxy/LittleProxy) that drops Netty. Each
connection runs on its own **virtual thread** (Project Loom) and uses plain blocking socket I/O.
- **Runtime:** JDK 21 or newer (also tested on JDK 25).
- **Dependencies:** none at runtime. Logging goes through `System.Logger`, which can be routed to
SLF4J/Log4j with the usual bridges. Put the `zstd-decoder` jar on the class path as well to
decode `zstd` bodies, the `http2-codec` jar for [HTTP/2](https://mahmoudimus.github.io/MicroProxy/guides/http2/) to clients and servers, and
the `quic` and `http3-codec` jars for [HTTP/3](https://mahmoudimus.github.io/MicroProxy/guides/http3/) to clients of a reverse proxy
and to servers (`http3-codec` alone runs [frame interceptors over HTTP/3 streams](https://mahmoudimus.github.io/MicroProxy/guides/frame-interception/#http3-frames)).
- **Size:** about 10k lines of main code (including Javadoc and a DNSSEC resolver) plus a vendored
Brotli codec, compared with LittleProxy's 11k lines plus Netty and dnssec4j.
- **Scripting (optional):** the `microproxy-starlark` module drives the proxy from a
[Starlark](https://github.com/bazelbuild/starlark) script (see [Scripting](https://mahmoudimus.github.io/MicroProxy/guides/scripting/)).
- **OpenTelemetry (optional):** the `microproxy-otel` module traces exchanges and exports traces,
metrics and logs over OTLP, with no dependencies either (see [OpenTelemetry](https://mahmoudimus.github.io/MicroProxy/observability/opentelemetry/)).
The modules, their Maven coordinates and the self-contained `-all` jar are listed in
[Install](https://mahmoudimus.github.io/MicroProxy/getting-started/install/#modules-and-artifacts).
## Design principles
- **No dependencies.** The core, the codecs, QUIC, OpenTelemetry export and the DNSSEC resolver
use only the JDK. Optional features are optional jars, found on the class path when present;
only the Starlark module depends on anything (the interpreter's Guava).
- **Virtual threads, not event loops.** Each connection is one virtual thread running a
straight-line loop; blocking a write is the backpressure. See
[Why virtual threads instead of Netty](https://mahmoudimus.github.io/MicroProxy/architecture/#why-virtual-threads-instead-of-netty)
for the design and its measured costs.
- **LittleProxy's filter API.** `HttpFiltersSource`, `HttpFilters`, `ActivityTracker`,
`ChainedProxyManager`, `MitmManager` and `ProxyAuthenticator` keep LittleProxy's shape, so
existing filters port with a package rename; `withLittleProxyCompatibility()` restores
LittleProxy's behaviour for HTTP/1 clients where MicroProxy differs, one
[protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) field at a time if need be. See
[Migrating from LittleProxy](https://mahmoudimus.github.io/MicroProxy/migrating-from-littleproxy/).
- **Envoy's and mitmproxy's vocabularies.** Access logs, stats, response flags and the admin API
use Envoy's names; interception rules, addons, flow filters, HAR and replay follow mitmproxy.
## Features
Each feature links to the guide that describes it.
| | |
|---|---|
| [HTTP/1.0 and 1.1 proxying](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/) | keep-alive on both sides, pipelining, chunked bodies and trailers, `Expect: 100-continue` (a `100` is sent for servers that ignore it), 1xx pass-through, re-chunking of close-delimited responses, de-chunking for HTTP/1.0 clients, stale keep-alive retry |
| [HTTP/2 to clients](https://mahmoudimus.github.io/MicroProxy/guides/http2/) | on by default (`--no-http2` turns it off; needs the `http2-codec` jar, else HTTP/1.1): clients that negotiate `h2` on intercepted TLS or the proxy TLS listener get HTTP/2, each stream an exchange of its own with the same filters, cache, failure answers and logging; hardened against stream, reset and frame floods. `--http2-cleartext` adds `h2c` with prior knowledge on the plain listener, and `--http2-cleartext-only` makes it an `h2c`-only listener. A forward proxy with `--masque` or `--http3` takes CONNECT-UDP (RFC 9298) over extended `CONNECT`, its datagrams in capsules |
| [HTTP/2 to servers](https://mahmoudimus.github.io/MicroProxy/guides/http2/#http2-to-servers) | on by default (`--no-http2-upstream` turns it off): servers that negotiate `h2` get every exchange for them as a stream on a shared connection, within their stream limit; trailers end to end and bidirectional streaming, so gRPC works through the proxy ([tested with grpcio](https://mahmoudimus.github.io/MicroProxy/guides/http2/#grpc-interop-grpcio)); connection coalescing across names a server's certificate covers (`--http2-coalesce`, RFC 9113 and RFC 8336's ORIGIN frame) |
| [HTTP/3](https://mahmoudimus.github.io/MicroProxy/guides/http3/) | optional, over the proxy's own QUIC (`quic` module): a reverse proxy serves HTTP/3 on UDP (`--http3`), each request stream an exchange of its own, advertised with `Alt-Svc`; a forward proxy serves it next to its TLS listener, with `CONNECT`, WebSockets (RFC 9220) and MASQUE's CONNECT-UDP (RFC 9298); servers that advertise `h3` get their exchanges, WebSockets included, over one shared QUIC connection (`--http3-upstream`), falling back to TCP; QPACK with dynamic tables, hardened limits, live frame interception |
| [Filters](https://mahmoudimus.github.io/MicroProxy/guides/filters/) | `HttpFilters` / `HttpFiltersSource` with the same hooks as LittleProxy, streaming or buffered (`getMaximumRequestBufferSizeInBytes` / `getMaximumResponseBufferSizeInBytes`); several sources run in order as an `HttpFiltersChain` (`plusFiltersSource`) |
| [Protocol policies](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) | where MicroProxy can behave in more than one way (the order of the filter hooks, the `CONNECT` answer, TLS to servers), a policy per client protocol chooses: LittleProxy's behaviour for HTTP/1.1 clients, say, and MicroProxy's for HTTP/2 and HTTP/3 |
| [TLS origination](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-origination-https-uris-without-connect) | `https://` URIs asked for without `CONNECT` (absolute-form over plain HTTP, `:scheme https` on `h2c`) are fetched over TLS the proxy opens, with the upstream trust, pooling, HTTP/2 and HTTP/3 of intercepted sessions, and through chained proxies' tunnels; `--no-originate-tls` refuses them |
| [CONNECT](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/) | byte tunnel with idle timeout and half-close; optional TCP hooks see, rewrite, drop and add the bytes of every raw tunnel, and of intercepted TLS that does not carry HTTP (SMTPS, IMAPS, MQTT over TLS), as mitmproxy's `tcp_message` |
| [MITM](https://mahmoudimus.github.io/MicroProxy/guides/mitm/) | `MitmManager`; `CertificateAuthorityMitmManager` issues per-host certificates on demand (EC P-256). Its SANs copy the real server's DNS names. Only the JDK is used, through a small built-in X.509/DER encoder (`org.microproxy.tls.CertificateBuilder`). CA, upstream trust and client certificate can be chosen per client connection |
| [ClientHello inspection](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello) | after `CONNECT`, the client's ClientHello is read without consuming it (SNI, ALPN, versions, cipher suites), and interception is decided by it: ignore / allow host rules as in mitmproxy, `MitmManager` and filter hooks; plain HTTP after `CONNECT` is served as HTTP, other protocols are tunnelled; ALPN is mirrored between client and server |
| [Transparent](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/) and [reverse](https://mahmoudimus.github.io/MicroProxy/guides/reverse-proxy/) modes | TLS redirected to the proxy without `CONNECT` is routed by SNI; `--mode reverse:https://host` serves one upstream over HTTP, HTTPS or raw TCP |
| [SOCKS server](https://mahmoudimus.github.io/MicroProxy/guides/socks/) | `--mode socks5` serves SOCKS5 (no authentication, or username/password through the `ProxyAuthenticator`) and SOCKS4a clients, whose requests go through the same flow as a `CONNECT`: filters, chained proxies, ClientHello inspection, interception, plain HTTP served as HTTP; `--socks` serves them on the regular listener too |
| [Chained proxies](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/) | HTTP (with Basic credentials, optionally over TLS, over HTTP/1.1 or [HTTP/2](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#http2-to-chained-proxies)), SOCKS4a, SOCKS5 (with username/password); falls back to the next proxy or a direct connection, optionally with exponential backoff between attempts. `UpstreamProxyManager` configures them from proxy URLs, `NO_PROXY` rules or the environment |
| [HTTP cache](https://mahmoudimus.github.io/MicroProxy/guides/caching/) | RFC 9111 shared cache in memory or on disk, with revalidation, `Vary`, stale responses when servers are unreachable, and an offline mode |
| [WARC recording](https://mahmoudimus.github.io/MicroProxy/guides/warc/) | `WarcRecorder` archives traffic with servers as WARC 1.1 files for replay tools, and `WarcReader` reads them back |
| [mitmproxy-style addons](https://mahmoudimus.github.io/MicroProxy/guides/addons/) | `MapLocal`, `MapRemote`, `ModifyBody`, `ModifyHeaders`, `BlockList`, `AntiCache` and `StickyCookie`, with mitmproxy's option syntax and flow filter expressions (`~d example.com & !~m GET`), from Java or the command line |
| [HAR export and replay](https://mahmoudimus.github.io/MicroProxy/guides/addons/#har-files) | `HarRecorder` writes HAR 1.2 files; `ServerReplay` answers requests from recorded HAR or WARC responses, `ClientReplay` sends recorded requests again through the proxy |
| [Body rewriting](https://mahmoudimus.github.io/MicroProxy/guides/rewriting-bodies/) | `HttpBodies` decodes gzip, deflate, Brotli and (with `zstd-decoder`) zstd bodies and re-encodes them with the right charset; `RewriteRules` edits headers and text bodies declaratively, buffering only the responses it rewrites |
| [Scripting](https://mahmoudimus.github.io/MicroProxy/guides/scripting/) | optional module: `on_request` / `on_response` / `upstream` / `allow_mitm` / `on_failure` / `authenticate` / `on_tcp_message` hooks in Starlark, sandboxed, with hot reload |
| [Proxy authentication](https://mahmoudimus.github.io/MicroProxy/guides/authentication/) | `ProxyAuthenticator`: Basic by default, or any scheme (Bearer tokens, API keys) with custom challenges; per connection or per request |
| [TLS listener](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-listener) | `withSslContextSource(...)`, optional client-certificate auth |
| [TLS protocol pinning](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#tls-protocol-versions) | every TLS connection (listener, both sides of interception, TLS chained proxies) allows only TLS 1.3 and 1.2 by default; `withTlsProtocols(...)` changes it |
| [PROXY protocol](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#proxy-protocol) | accept v1 and v2 (read before TLS on a TLS listener), send v1 to the final server: first on a direct connection, through the tunnel after an HTTP chained proxy accepts the CONNECT; not sent through SOCKS chained proxies or with plain requests to an HTTP chained proxy |
| [WebSockets](https://mahmoudimus.github.io/MicroProxy/guides/websockets/) | HTTP/1 `Upgrade` and HTTP/2 extended `CONNECT` (RFC 8441), with the same frame and whole-message hooks in both directions, reading through permessage-deflate |
| [Shared connection pool](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#shared-server-connection-pool) | optional server connection reuse across clients, with limits and idle eviction |
| [DNSSEC](https://mahmoudimus.github.io/MicroProxy/guides/dnssec/) | optional validating resolver, with no dependencies |
| [Access logs](https://mahmoudimus.github.io/MicroProxy/observability/access-logs/) | `ActivityLogger`: Envoy-compatible format strings and JSON, Envoy's response flags, filters, and one line per exchange, tunnel or UDP and QUIC flow; LittleProxy's nine formats as presets |
| [Metrics](https://mahmoudimus.github.io/MicroProxy/observability/metrics/) | Envoy's stat names and tags (`http.*`, `cluster.*`, `listener.*`, `server.*`, `http2.*`, `http3.*`, ...) in a `StatsRegistry`, as Prometheus text, Envoy's `/stats` text and JSON, or pushed to statsd and DogStatsD; per-protocol stats (QUIC, transparent QUIC, SOCKS, CONNECT-UDP, tunnels, WebSockets) and the hooks'; bounded clusters; scripts' own `stats`; qlog files per QUIC connection |
| [OpenTelemetry](https://mahmoudimus.github.io/MicroProxy/observability/opentelemetry/) | optional module: W3C Trace Context (and B3, Jaeger) propagation and `X-Request-Id`, a SERVER span per HTTP/1, HTTP/2 and HTTP/3 exchange, a CLIENT span per attempt to reach the server (retries and chained proxy failover included), INTERNAL spans for tunnels and flows, sampling, and OTLP/HTTP export (protobuf or JSON) of traces, the stats as metrics and access logs as log records, from `OTEL_*` variables or `--otel` |
| [Admin interface](https://mahmoudimus.github.io/MicroProxy/observability/admin/) | Envoy's admin API on a listener of its own: `/stats`, `/stats/prometheus`, `/ready`, `/server_info`, `/listeners`, `/clusters`, `/config_dump` (secrets redacted), `/logging`, health check, `/drain_listeners` and `/quitquitquit`, with an optional token; server states, gradual or immediate draining, and a drain time for graceful shutdown |
| [Request/response logging](https://mahmoudimus.github.io/MicroProxy/observability/request-logging/) | `HttpLogger` dumps whole messages (heads, the changes the proxy and filters made, bodies on request) as readable blocks or JSON lines, with redaction |
| [Content views](https://mahmoudimus.github.io/MicroProxy/guides/content-views/) | `org.microproxy.contentviews` renders bodies readably: protobuf without a schema (YAML with field numbers, as mitmproxy shows it), gRPC message by message with its trailers and `google.rpc.Status` details, msgpack, form data, GraphQL, Socket.IO; field names from a `protoc` descriptor set; `protobuf` and `grpc` modules for scripts |
| [Throttling](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#throttling) | global token bucket for server reads and writes, adjustable at runtime |
| [Concurrency limiting](https://mahmoudimus.github.io/MicroProxy/guides/concurrency-limiting/) | `ConcurrencyLimiter` caps the exchanges in progress per client, user, target host or any key, with a bounded wait queue, `429` answers, a shadow mode and metrics |
| [Activity tracking](https://mahmoudimus.github.io/MicroProxy/observability/activity-tracking/) | `ActivityTracker` for connections, requests, responses (with their source), bytes, per-exchange timings and server failures |
| [HTTP/3 bypass](https://mahmoudimus.github.io/MicroProxy/guides/http3/#alt-svc-which-clients-may-move-to-http3) | removes `h3` alternatives from `Alt-Svc` by default when intercepting or transparent, so clients of an explicit or transparent proxy stay on TCP through it rather than move to QUIC |
| [Privacy](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#removing-tracing-headers) | optionally removes distributed tracing headers (W3C Trace Context and Baggage, B3 and other common trace headers) and any other named headers from requests sent upstream, after all filters |
| [Hardening](https://mahmoudimus.github.io/MicroProxy/security/) | rejects `Transfer-Encoding` + `Content-Length`, conflicting lengths, obs-fold in requests, and oversized lines and headers; header values are validated against CR/LF injection; Host is replaced by the absolute-form authority |
## Where to go next
- **Run it:** [Install](https://mahmoudimus.github.io/MicroProxy/getting-started/install/), then the [Quick start](https://mahmoudimus.github.io/MicroProxy/getting-started/quick-start/).
- **Embed it:** [Your first filter](https://mahmoudimus.github.io/MicroProxy/getting-started/first-filter/) and the
[Java API (Javadoc)](https://mahmoudimus.github.io/MicroProxy/java-api/).
- **Script it:** [Your first Starlark script](https://mahmoudimus.github.io/MicroProxy/getting-started/first-script/), then
[Scripting with Starlark](https://mahmoudimus.github.io/MicroProxy/guides/scripting/) and the [Starlark API](https://mahmoudimus.github.io/MicroProxy/reference/starlark-api/).
- **Look things up:** [command-line flags](https://mahmoudimus.github.io/MicroProxy/reference/cli/),
[configuration properties](https://mahmoudimus.github.io/MicroProxy/reference/properties/), [stats](https://mahmoudimus.github.io/MicroProxy/reference/stats/),
[access-log operators](https://mahmoudimus.github.io/MicroProxy/reference/access-log-format/), [response flags](https://mahmoudimus.github.io/MicroProxy/reference/response-flags/)
and [admin endpoints](https://mahmoudimus.github.io/MicroProxy/reference/admin-api/).
- **Operate it:** [Observability](https://mahmoudimus.github.io/MicroProxy/observability/access-logs/) and [Security](https://mahmoudimus.github.io/MicroProxy/security/).
- **Compare it:** [Performance against other proxies](https://mahmoudimus.github.io/MicroProxy/project/performance/) measures MicroProxy
against LittleProxy, Envoy, a Go and a Rust proxy and Squid, with interactive charts and the raw
data.
- **For AI coding agents:** [`llms.txt`](https://mahmoudimus.github.io/MicroProxy/llms.txt) lists
every page with a one-line description, and
[`llms-full.txt`](https://mahmoudimus.github.io/MicroProxy/llms-full.txt) is the whole site as
one Markdown file.
# Install
How to get MicroProxy: the Maven and Gradle coordinates of each module, the self-contained `-all`
jar with every optional feature built in, and how to build the launcher from source. Read this
first, then follow the [Quick start](https://mahmoudimus.github.io/MicroProxy/getting-started/quick-start/).
## Requirements
- **Java:** JDK 21 or newer (also tested on JDK 25). The proxy uses virtual threads, which are
final in JDK 21.
- **Dependencies:** none at runtime. Logging goes through `System.Logger`, which can be routed to
SLF4J/Log4j with the usual bridges (`slf4j-jdk-platform-logging`, `log4j-jpl`).
## Modules and artifacts
Every module is published to Maven Central under the `io.github.mahmoudimus` group, at the same
version. The core has no required dependencies; the codecs and QUIC are optional jars it uses
when they are on the class path.
| Module | Artifact | Contents |
|---|---|---|
| `zstd-decoder/` | `io.github.mahmoudimus:zstd-decoder` | a standalone pure-Java Zstandard decoder ([README](https://github.com/mahmoudimus/MicroProxy/blob/main/zstd-decoder/README.md)) |
| `http2-codec/` | `io.github.mahmoudimus:http2-codec` | a standalone HTTP/2 frame codec and HPACK implementation ([README](https://github.com/mahmoudimus/MicroProxy/blob/main/http2-codec/README.md)) |
| `http3-codec/` | `io.github.mahmoudimus:http3-codec` | a standalone HTTP/3 frame codec and QPACK implementation, used by HTTP/3 and the HTTP/3 frame pipeline ([README](https://github.com/mahmoudimus/MicroProxy/blob/main/http3-codec/README.md)) |
| `quic/` | `io.github.mahmoudimus:quic` | a standalone QUIC transport with its own TLS 1.3 handshake, ported from quinn and rustls, used by HTTP/3 ([README](https://github.com/mahmoudimus/MicroProxy/blob/main/quic/README.md)) |
| `microproxy/` | `io.github.mahmoudimus:microproxy` | the proxy; no required dependencies (`zstd-decoder`, `http2-codec`, `http3-codec` and `quic` are optional) |
| `microproxy-otel/` | `io.github.mahmoudimus:microproxy-otel` | OpenTelemetry: W3C, B3 and Jaeger propagation, spans per exchange and attempt, and OTLP/HTTP export of traces, metrics and access logs; depends on the core only ([OpenTelemetry](https://mahmoudimus.github.io/MicroProxy/observability/opentelemetry/)) |
| `microproxy-starlark/` | `io.github.mahmoudimus:microproxy-starlark` | Starlark scripting; depends on the core and Guava |
Which optional jars a feature needs:
| Feature | Jars besides `microproxy` |
|---|---|
| `zstd` bodies (`Content-Encoding: zstd`, gRPC's `zstd` encoding in content views) | `zstd-decoder` |
| [HTTP/2](https://mahmoudimus.github.io/MicroProxy/guides/http2/) to clients and servers (on by default: `--http2`, `--http2-upstream`; and `--http2-cleartext`) | `http2-codec` |
| [HTTP/3 and QUIC](https://mahmoudimus.github.io/MicroProxy/guides/http3/), [CONNECT-UDP](https://mahmoudimus.github.io/MicroProxy/guides/masque/), [transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic) | `quic` and `http3-codec` |
| [Frame interceptors over HTTP/3 streams](https://mahmoudimus.github.io/MicroProxy/guides/frame-interception/#http3-frames) (`Http3FramePipeline`) | `http3-codec` |
| [OpenTelemetry](https://mahmoudimus.github.io/MicroProxy/observability/opentelemetry/) (`--otel`, `OTEL_*`) | `microproxy-otel` |
| [Starlark scripting](https://mahmoudimus.github.io/MicroProxy/guides/scripting/) (`--script`) | `microproxy-starlark` (and its Guava dependency) |
A proxy started with HTTP/2 or HTTP/3 enabled but without the jars it needs fails at startup with
an `IllegalStateException` that says so; without those features, nothing loads them. HTTP/2 to
clients and servers, on by default, is the exception: left at its default without `http2-codec`,
it falls back to HTTP/1.1 (logged once at `INFO`), so the `microproxy` jar alone still runs with
`--mitm`; only `--http2`, `--http2-upstream` or `--http2-cleartext` given explicitly fail.
## Maven
```xml
io.github.mahmoudimusmicroproxy0.4.1-SNAPSHOTio.github.mahmoudimushttp2-codec0.4.1-SNAPSHOT
```
Add `quic` and `http3-codec` for HTTP/3, `zstd-decoder` for `zstd` bodies, `microproxy-otel` for
OpenTelemetry and `microproxy-starlark` for scripting, all at the same version.
## Gradle
```kotlin
dependencies {
implementation("io.github.mahmoudimus:microproxy:0.4.1-SNAPSHOT")
// Optional, at the same version:
implementation("io.github.mahmoudimus:http2-codec:0.4.1-SNAPSHOT")
implementation("io.github.mahmoudimus:http3-codec:0.4.1-SNAPSHOT")
implementation("io.github.mahmoudimus:quic:0.4.1-SNAPSHOT")
}
```
## The self-contained all jar
`microproxy-starlark` also builds an executable jar with the classifier `all` that bundles the
launcher, scripting, zstd, HTTP/2, QUIC, HTTP/3 and OpenTelemetry: one file to run with
`java -jar`. It is attached to every [GitHub release](https://github.com/mahmoudimus/MicroProxy/releases)
(with a `SHA256SUMS` file) and published to Maven Central as
`io.github.mahmoudimus:microproxy-starlark::all`.
```bash
MICROPROXY_VERSION=0.4.1-SNAPSHOT
curl -LO https://github.com/mahmoudimus/MicroProxy/releases/download/v${MICROPROXY_VERSION}/microproxy-starlark-${MICROPROXY_VERSION}-all.jar
java -jar microproxy-starlark-${MICROPROXY_VERSION}-all.jar --help
```
## The launcher
`org.microproxy.Launcher` is the command-line entry point, and the `Main-Class` of both the
`microproxy` jar and the `-all` jar. `--help` prints every option; the
[command-line reference](https://mahmoudimus.github.io/MicroProxy/reference/cli/) lists them with links to their guides, and
[configuration properties](https://mahmoudimus.github.io/MicroProxy/reference/properties/) lists the keys of a `--config` file.
Optional modules add their own options when their jars are on the class path (`--script` from
`microproxy-starlark`, `--otel` from `microproxy-otel`).
## Building from source
```bash
git clone https://github.com/mahmoudimus/MicroProxy.git && cd MicroProxy
MICROPROXY_VERSION=0.4.1-SNAPSHOT
mvn package # every module, with the tests
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080
```
`mvn -DskipTests package` skips the tests. See [Building and testing](https://mahmoudimus.github.io/MicroProxy/project/building-and-testing/)
for the test suites and the release process.
## API documentation
The Javadoc of every published module is on this site at [`/api/`](https://mahmoudimus.github.io/MicroProxy/api/) ([Java API](https://mahmoudimus.github.io/MicroProxy/java-api/)), and each release's
Javadoc is also on [javadoc.io](https://javadoc.io/doc/io.github.mahmoudimus/microproxy), since
the jars on Maven Central ship with their Javadoc.
# Quick start
Run MicroProxy from the command line in a minute, then embed it in a Java program. Use this page
for copy-pasteable commands; the [command-line reference](https://mahmoudimus.github.io/MicroProxy/reference/cli/) lists every flag.
## A forward proxy in one command
Build the jars (or download them: see [Install](https://mahmoudimus.github.io/MicroProxy/getting-started/install/)), start the proxy on port 8080, and
send a request through it:
```bash
MICROPROXY_VERSION=0.4.1-SNAPSHOT
mvn -q -DskipTests package
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080
```
The launcher prints where it listens (after the same line from its logger):
```text
MicroProxy listening on localhost/127.0.0.1:8080
```
In another terminal:
```bash
curl -x http://127.0.0.1:8080 -I http://example.com/
```
The response carries the proxy's `Via` header (`Via: 1.1 `, with the server's HTTP
version). By default the proxy listens on loopback only; `--server` listens on all interfaces.
## Intercepting HTTPS
```bash
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080 --mitm
curl -x http://127.0.0.1:8080 --cacert microproxy-ca.pem https://example.com/
```
`--mitm` creates (or reuses) a CA in `microproxy-ca.p12` and writes its certificate to
`microproxy-ca.pem`. To intercept HTTPS without errors, add that certificate to your client's
trust store. See [MITM, certificates and TLS](https://mahmoudimus.github.io/MicroProxy/guides/mitm/).
## More command lines
```bash
MICROPROXY_VERSION=0.4.1-SNAPSHOT
mvn package
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080 --mitm # intercept HTTPS
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080 --dnssec --activity-log-format clf
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080 --mitm --log-http headers # dump traffic
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080 --mitm --save-har flows.har # record, then
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080 --mitm --server-replay flows.har # replay offline
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8080 --mitm --ignore-hosts '\.bank\.example:443$'
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 8443 --reverse https://example.com # reverse proxy
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --port 1080 --mode socks5 --mitm # SOCKS5 server
java -jar microproxy/target/microproxy-${MICROPROXY_VERSION}.jar --help
# With zstd decoding:
java -cp microproxy/target/microproxy-${MICROPROXY_VERSION}.jar:\
zstd-decoder/target/zstd-decoder-${MICROPROXY_VERSION}.jar org.microproxy.Launcher --port 8080
# With HTTP/2 to clients on intercepted TLS and to servers, on by default once the http2-codec jar
# is on the class path (without it, the proxy speaks HTTP/1.1; --no-http2 and --no-http2-upstream
# turn HTTP/2 off):
java -cp microproxy/target/microproxy-${MICROPROXY_VERSION}.jar:\
http2-codec/target/http2-codec-${MICROPROXY_VERSION}.jar org.microproxy.Launcher --port 8080 --mitm
# The same launcher with scripting, zstd, HTTP/2, QUIC and HTTP/3 built in (one self-contained jar):
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080 --script proxy.star
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080 --mitm
# Traces, metrics and access logs to an OpenTelemetry Collector (OTLP/HTTP on 4318):
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080
# A reverse proxy serving HTTPS, HTTP/2 and HTTP/3 (on UDP 8443), speaking HTTP/3 to servers that offer it:
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8443 \
--mode reverse:https://example.com --listener-keystore site.p12 --listener-keystore-password secret \
--http3 --http3-upstream
# A forward proxy on TLS (8443) that also serves HTTP/3 on UDP 8443: CONNECT, WebSockets and
# CONNECT-UDP (MASQUE) for HTTP/3 clients and, over TCP, for HTTP/2 clients, the QUIC inside their
# UDP flows intercepted too:
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8443 \
--listener-keystore proxy.p12 --listener-keystore-password secret --http3 \
--mitm --masque-intercept-quic
# A plain forward proxy (8080) that tunnels UDP too, without HTTP/3: CONNECT-UDP over h2c and as
# an HTTP/1.1 upgrade:
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080 \
--http2-cleartext --masque
# A transparent proxy for redirected TCP (8080) and QUIC (UDP 8443), intercepting HTTP/3 too, and
# other QUIC stream by stream through a script's on_quic_*() hooks:
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080 --server \
--transparent --transparent-quic-port 8443 --mitm --http3-upstream --script quic.star
```
Each of these is described in its guide: [MITM](https://mahmoudimus.github.io/MicroProxy/guides/mitm/),
[DNSSEC](https://mahmoudimus.github.io/MicroProxy/guides/dnssec/), [access logs](https://mahmoudimus.github.io/MicroProxy/observability/access-logs/),
[request logging](https://mahmoudimus.github.io/MicroProxy/observability/request-logging/),
[HAR and replay](https://mahmoudimus.github.io/MicroProxy/guides/addons/#har-files),
[ignoring hosts](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#ignoring-and-allowing-hosts),
[reverse proxy](https://mahmoudimus.github.io/MicroProxy/guides/reverse-proxy/), [SOCKS](https://mahmoudimus.github.io/MicroProxy/guides/socks/),
[HTTP/2](https://mahmoudimus.github.io/MicroProxy/guides/http2/), [HTTP/3](https://mahmoudimus.github.io/MicroProxy/guides/http3/), [CONNECT-UDP](https://mahmoudimus.github.io/MicroProxy/guides/masque/),
[transparent mode](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/), [OpenTelemetry](https://mahmoudimus.github.io/MicroProxy/observability/opentelemetry/)
and [scripting](https://mahmoudimus.github.io/MicroProxy/guides/scripting/).
## Embedded in a Java program
Add `io.github.mahmoudimus:microproxy` to your build ([Install](https://mahmoudimus.github.io/MicroProxy/getting-started/install/)) and start a proxy
with `MicroProxy.bootstrap()`:
```java
import org.microproxy.HttpProxyServer;
import org.microproxy.MicroProxy;
public class Proxy {
public static void main(String[] args) {
HttpProxyServer proxy = MicroProxy.bootstrap()
.withPort(8080)
.start();
System.out.println("listening on " + proxy.getListenAddress());
}
}
```
The proxy runs until `proxy.stop()` (or the JVM exits). Every flag has a bootstrap method: the
guides show both, and the [configuration properties](https://mahmoudimus.github.io/MicroProxy/reference/properties/) give the same
settings in a file (`MicroProxy.bootstrapFromFile(path)`). Next:
[Your first filter](https://mahmoudimus.github.io/MicroProxy/getting-started/first-filter/) changes traffic as it passes.
# Your first filter
Write a Java filter that answers some requests itself and lets the rest through, then the same
filter with lambdas. Use this page to learn the filter API; [Filters and failure
responses](https://mahmoudimus.github.io/MicroProxy/guides/filters/) covers every hook.
## A filter class
Filters keep LittleProxy's shape: an `HttpFiltersSource` makes an `HttpFilters` per request, and
the `HttpFilters` overrides the hooks it needs. Returning a response from
`clientToProxyRequest` answers the request without contacting the server; returning `null` lets
it through.
```java
import org.microproxy.FlowContext;
import org.microproxy.HttpFilters;
import org.microproxy.HttpFiltersAdapter;
import org.microproxy.HttpFiltersSourceAdapter;
import org.microproxy.HttpProxyServer;
import org.microproxy.MicroProxy;
import org.microproxy.http.DefaultFullHttpResponse;
import org.microproxy.http.HttpObject;
import org.microproxy.http.HttpRequest;
import org.microproxy.http.HttpResponse;
import org.microproxy.http.HttpResponseStatus;
import org.microproxy.http.HttpVersion;
public class BlockingProxy {
public static void main(String[] args) {
HttpProxyServer proxy = MicroProxy.bootstrap()
.withPort(8080)
.withFiltersSource(new HttpFiltersSourceAdapter() {
@Override
public HttpFilters filterRequest(HttpRequest originalRequest, FlowContext ctx) {
return new HttpFiltersAdapter(originalRequest, ctx) {
@Override
public HttpResponse clientToProxyRequest(HttpObject httpObject) {
if (httpObject instanceof HttpRequest r && r.uri().contains("/blocked")) {
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.FORBIDDEN, "blocked by proxy");
}
return null; // continue proxying
}
};
}
})
.start();
System.out.println("listening on " + proxy.getListenAddress());
}
}
```
Run it with the `microproxy` jar on the class path, and try it:
```bash
MICROPROXY_VERSION=0.4.1-SNAPSHOT
java -cp microproxy/target/microproxy-${MICROPROXY_VERSION}.jar BlockingProxy.java
```
```console
$ curl -s -i -x http://127.0.0.1:8080 http://example.com/blocked
HTTP/1.1 403 Forbidden
...
blocked by proxy
```
Any other URL is fetched from the server as usual.
## The same filter with lambdas
For small filters, `HttpFilters.builder()` takes one lambda per hook, and `HttpFiltersSource` is a
functional interface. The built filters are also a filters source that returns them for every
request (`org.microproxy.HttpFiltersBuilder`):
```java
HttpFiltersBuilder.Built filters = HttpFilters.builder()
.onRequest(req -> req.uri().contains("/blocked")
? new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, "blocked by proxy")
: null)
.build();
MicroProxy.bootstrap().withPort(8080).withFiltersSource(filters).start();
```
See [Filters from lambdas](https://mahmoudimus.github.io/MicroProxy/guides/filters/#filters-from-lambdas) for every builder hook.
## Next steps
- [Filters and failure responses](https://mahmoudimus.github.io/MicroProxy/guides/filters/): every hook, buffering, failure answers.
- [Rewriting bodies](https://mahmoudimus.github.io/MicroProxy/guides/rewriting-bodies/): decode, edit and re-encode bodies.
- [Addons, HAR and replay](https://mahmoudimus.github.io/MicroProxy/guides/addons/): ready-made filters (`MapRemote`, `BlockList`, ...).
- [MITM, certificates and TLS](https://mahmoudimus.github.io/MicroProxy/guides/mitm/): see inside HTTPS.
- [Migrating from LittleProxy](https://mahmoudimus.github.io/MicroProxy/migrating-from-littleproxy/), if you have LittleProxy filters.
# Your first Starlark script
Drive the proxy from a [Starlark](https://github.com/bazelbuild/starlark) script (a sandboxed
Python dialect) instead of Java: answer a request, add headers, and watch the script reload when
you edit it. Use this page to get started; [Scripting with Starlark](https://mahmoudimus.github.io/MicroProxy/guides/scripting/) and
the [Starlark API](https://mahmoudimus.github.io/MicroProxy/reference/starlark-api/) cover everything else.
## Write the script
Save this as `hello.star`. Every hook is optional; these two run for every request and every
response:
```python
def on_request(req, ctx):
if req.path == "/hello":
return response(200, "hello from Starlark\n") # answer without contacting the server
req.headers["X-Client"] = ctx.client_ip # add a header to what goes upstream
def on_response(req, res, ctx):
res.headers["X-Scripted"] = "yes"
```
## Run it
Scripting is in the `microproxy-starlark` module; its `-all` jar has the launcher and every
optional feature built in ([Install](https://mahmoudimus.github.io/MicroProxy/getting-started/install/#the-self-contained-all-jar)):
```bash
MICROPROXY_VERSION=0.4.1-SNAPSHOT
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080 --script hello.star
```
```console
$ curl -s -x http://127.0.0.1:8080 http://example.com/hello
hello from Starlark
$ curl -s -o /dev/null -D - -x http://127.0.0.1:8080 http://example.com/ | grep -i x-scripted
X-Scripted: yes
```
Add `--mitm` to run the same hooks on the requests inside HTTPS connections (see
[MITM, certificates and TLS](https://mahmoudimus.github.io/MicroProxy/guides/mitm/)).
## Edit it while it runs
The proxy re-reads the script when the file changes (checked at most once a second). An edit that
does not compile is logged and the previous version stays in use; `--script-no-reload` turns
reloading off. Scripts are sandboxed: no file, network or process access, and each hook call has
a budget of steps, time and memory ([Budgets and measurements](https://mahmoudimus.github.io/MicroProxy/reference/starlark-api/#script-budgets)).
## From Java
The same script, embedded: one `ScriptedProxy` is the filters source and the chained proxy
manager (for an `upstream` hook).
```java
ScriptedProxy script = ScriptedProxy.builder(Path.of("hello.star")).build();
MicroProxy.bootstrap().withPort(8080).withFiltersSource(script).withChainProxyManager(script).start();
```
## Next steps
- [Scripting with Starlark](https://mahmoudimus.github.io/MicroProxy/guides/scripting/): routing (`upstream`), interception decisions,
failures, authentication, WebSockets, frames, TCP, UDP and QUIC hooks, with examples.
- [Starlark API](https://mahmoudimus.github.io/MicroProxy/reference/starlark-api/): every hook, object, built-in and module.
# Forward proxy
The default mode: clients are configured to use MicroProxy as their HTTP proxy and send it
absolute-form requests and `CONNECT`s. This page covers what plain HTTP/1 proxying does, the TLS
listener (an HTTPS proxy), TLS origination for `https://` URIs sent without `CONNECT`, the PROXY
protocol, bandwidth throttling, removing tracing headers and the shared server connection pool.
Interception of HTTPS is in [MITM, certificates and TLS](https://mahmoudimus.github.io/MicroProxy/guides/mitm/).
## Running a forward proxy
```bash
java -jar microproxy.jar --port 8080 # loopback only
java -jar microproxy.jar --port 8080 --server # every interface
curl -x http://127.0.0.1:8080 http://example.com/
curl -x http://127.0.0.1:8080 https://example.com/ # a CONNECT tunnel, untouched without --mitm
```
```java
HttpProxyServer proxy = MicroProxy.bootstrap()
.withPort(8080)
.withAllowLocalOnly(false) // --server
.start();
```
## HTTP/1.0 and 1.1 proxying
Keep-alive on both sides, pipelining, chunked bodies and trailers, `Expect: 100-continue` (a
`100` is sent for servers that ignore it), 1xx pass-through, re-chunking of close-delimited
responses, de-chunking for HTTP/1.0 clients, and a retry when a kept-alive server connection
turns out to be stale. The proxy adds `Via` (named by `--proxy-alias`, `proxy_alias`,
`withProxyAlias`; the host name by default) and removes hop-by-hop headers, except in
[transparent mode](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/). Requests in origin form (`GET /path`) are refused unless
`--allow-requests-to-origin-server` (`allow_requests_to_origin_server`) allows them.
Requests are parsed strictly ([Security](https://mahmoudimus.github.io/MicroProxy/security/#request-parsing)), and when the proxy has
to answer itself it sends a short plain-text answer ([Failure responses](https://mahmoudimus.github.io/MicroProxy/guides/filters/#failure-responses)).
## CONNECT tunnels
A `CONNECT` opens a byte tunnel, with an idle timeout (`--idle-timeout`, 70 seconds by default)
and half-close. Without a MITM manager the tunnel is relayed untouched; with one, the client's
first bytes decide whether it is intercepted ([Deciding what to
intercept](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)). [TCP hooks](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/) can see
and rewrite the bytes of tunnels that are not intercepted.
## TLS listener
`withSslContextSource(...)` serves the proxy itself over TLS (an "HTTPS proxy"), and
`withAuthenticateSslClients(true)` requires client certificates on it. From the command line,
`--listener-keystore ` and `--listener-keystore-password ` (`listener_keystore`,
`listener_keystore_password`) serve TLS with a PKCS#12 or JKS key store's first key entry. The
listener also offers HTTP/2 (unless `--no-http2`), and with `--http3` a forward proxy serves HTTP/3 next to
it ([An HTTP/3 forward-proxy listener](https://mahmoudimus.github.io/MicroProxy/guides/http3/#an-http3-forward-proxy-listener)).
```java
MicroProxy.bootstrap()
.withPort(8443)
.withSslContextSource(new KeyStoreSslContextSource(Path.of("proxy.p12"), password))
.start();
```
## PROXY protocol
`--accept-proxy-protocol` (`allow_proxy_protocol=true`, `withAcceptProxyProtocol(true)`) requires a
PROXY protocol header (v1 or v2) on every inbound connection, read before anything else: before
TLS on a TLS listener, and before SOCKS, HTTP/2 prior knowledge or transparent TLS are told apart.
The source it names is the client from then on: `FlowContext.getClientAddress()`, `ClientDetails`,
access logs and traces (`%DOWNSTREAM_REMOTE_ADDRESS%`, `client.address`), and the header sent on
with `--send-proxy-protocol`. The load balancer's own address stays in
`%DOWNSTREAM_DIRECT_REMOTE_ADDRESS%` (`FlowContext.getDirectClientAddress()`), and the header's
destination, the address the client reached at the load balancer, in
`FlowContext.getProxyProtocolDestinationAddress()` (the proxy's own address stays in
`getLocalAddress()`; see [Addresses of a flow](https://mahmoudimus.github.io/MicroProxy/guides/filters/#addresses-of-a-flow)). A v1 `UNKNOWN`
header, and a v2 `LOCAL` one or one without addresses, leave the TCP peer as the client.
`--proxy-protocol-mode optional` (`allow_proxy_protocol=optional`,
`withAcceptProxyProtocol(ProxyProtocolMode.OPTIONAL)`) reads a header only when the connection
starts with one, for a listener that load balancers and clients reach directly, or during a
migration. The proxy looks at the first bytes without consuming them: `PROXY ` starts a v1
header and the 12-byte v2 signature a v2 one; anything else (an HTTP request, a TLS ClientHello, a
SOCKS greeting) is served as if PROXY protocol were off, its bytes untouched. A request is told
apart by its first byte or few, so it is not delayed.
> **Optional PROXY protocol lets any client choose its address.** In optional mode any client
> that can reach the listener can send a header and be logged, rate-limited and authorized as
> whatever address it names. Use it only where the clients that can connect directly are trusted
> (behind a load balancer, on a private network), and name the load balancers with
> `--proxy-protocol-trusted-sources 10.0.0.0/8,192.0.2.10` (`proxy_protocol_trusted_sources`,
> `withProxyProtocolTrustedSources(...)`).
With trusted sources, only those peers' headers count. A connection from another peer that starts
with a header is closed rather than served (with its header ignored, a misrouted or forged header
would go unnoticed, and the client would still get through); with a required header, every
connection from another peer is closed. Without trusted sources, every peer's header counts.
Once a connection's first byte has come (which may take the idle timeout, as a request's would),
the rest of the header must arrive within 5 seconds, or the idle timeout if that is shorter,
however slowly it trickles in; a connection that sends part of a signature or header and stalls is
closed then. The `proxy_proto.*` [stats](https://mahmoudimus.github.io/MicroProxy/reference/stats/) count the headers read, the
connections without one, malformed headers and untrusted peers.
`--send-proxy-protocol` (`send_proxy_protocol`, `withSendProxyProtocol(true)`) sends a PROXY
protocol v1 header to the final server: first on a direct connection, or through the tunnel once
an HTTP chained proxy has accepted the `CONNECT`. It is not sent where there is no tunnel to the
final server: through SOCKS chained proxies, or with plain requests forwarded to an HTTP chained
proxy. Server connections that carry the header belong to one client, so the shared server
connection pool is not used while sending is on.
## Throttling
A global token bucket limits the bandwidth of server reads and writes, in bytes per second (`0`
means unlimited): `--throttle `, `throttle_read_bytes_per_second` and
`throttle_write_bytes_per_second`, or `withThrottling(read, write)`. `HttpProxyServer.setThrottle`
changes the limits while the proxy runs.
## TLS origination: `https://` URIs without `CONNECT`
Browsers ask a forward proxy for `https://` pages with `CONNECT` and speak TLS to the server
themselves. Some clients instead hand the proxy the URI in plain HTTP and leave TLS to it: `GET
https://example.com/ HTTP/1.1` in absolute-form, an HTTP/2 stream with `:scheme https` on an
[h2c](https://mahmoudimus.github.io/MicroProxy/guides/http2/#h2c-with-prior-knowledge) connection or on the proxy's TLS listener, or a secure WebSocket
(`wss://`, or extended `CONNECT` with `:scheme https`). A forward proxy fetches these over TLS
that it opens to the server itself, as it does for the requests of an intercepted session:
- **Trust.** The server's certificate and name are checked with the MITM manager's server TLS
context (`CertificateAuthorityMitmManager`'s upstream trust, `--mitm-trust-all` included; a
manager chosen per client connection is asked as for interception), or with the JVM's default
trust store when there is no manager. A failed handshake is `ProxyFailure.TlsFailed`: a `502`
from the [failure responder](https://mahmoudimus.github.io/MicroProxy/guides/filters/#failure-responses), the `UF` response flag with
`proxy_failure:tls_failed` details, the handshake's error in `%UPSTREAM_TRANSPORT_FAILURE_REASON%`
(an untrusted issuer, a name the certificate does not cover; see
[Server TLS failures](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-tls-failures)), `on_failure` in scripts, and the
cluster's `ssl.connection_error`, as for an intercepted session's server. The server name and
verification host can differ from the URI's host
([Server name and verification host](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-name-and-verification-host)).
- **Connections.** They are the same kind of connection an intercepted session's requests use, and
pooled with them under the same keys, kept apart by target, route (direct, or which chained
proxy) and MITM manager: a client connection keeps its own, or, with the [shared
pool](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#shared-server-connection-pool) and `pool_shared_mitm_connections`, every client shares
them, whether they were made for an intercepted session or for a client's `https://` URI.
- **HTTP/2 and HTTP/3.** Unless `--no-http2-upstream`, the handshake offers `h2` and `http/1.1`, and a
server that picks `h2` gets the requests as streams on a shared connection, with [connection
coalescing](https://mahmoudimus.github.io/MicroProxy/guides/http2/#connection-coalescing) when it is on; with `--http3-upstream`, a server known to
speak HTTP/3 gets them over QUIC, on direct routes.
- **Chained proxies.** Through an HTTP chained proxy the request does not go in absolute-form, as
an `http://` one does: the proxy asks it for a tunnel to the server (`CONNECT host:443`) and runs
TLS inside, as a browser would, so the chained proxy never sees the request. SOCKS proxies give
a tunnel the same way, and `UpstreamProxyManager` picks its HTTPS upstream
(`--upstream-https-proxy`) for an `https://` URI.
- **What everyone sees.** Filters get the request as the client sent it, `https://...` included;
scripts see `req.scheme == "https"` and the `https://` URL in every hook; the server gets it in
origin-form with `Host` from the URI. Access logs record the `https://` request with the
server's TLS session (`%UPSTREAM_TLS_VERSION%`, `%UPSTREAM_PEER_SUBJECT%`, ...), stats count the
handshakes in the route's cluster (`cluster.direct.ssl.handshake`, ...), and the OpenTelemetry
SERVER span (`url.scheme`, `url.full`) and CLIENT span (`url.full`) say `https`.
This is on by default, as it has been since 0.2.0 (before, such a request went to the server's
port in plain text). `withTlsOrigination(false)` (`--no-originate-tls`, `originate_tls=false`)
turns it off: such requests are refused with `ProxyFailure.BadRequest` (`400 Bad Request:
https:// URIs need CONNECT (TLS origination is off)`, flag `DPE`) before any connection is made.
Turn it off when plain-HTTP clients should reach TLS servers only through tunnels, as operators
may expect of them: with TLS origination the proxy reads and handles in the clear what host rules
(`--ignore-hosts`, `--allow-hosts`) and `proxyToServerAllowMitm` would have kept from it on a
`CONNECT`, and on the plain listener the request, cookies and credentials included, crosses the
network between client and proxy in plain text. An `https://` URI that a filter or script gives
a request (`MapRemote`, `req.uri = ...`) is fetched over TLS either way: that is the operator's
choice, not the client's. So are the requests of intercepted sessions, and a reverse proxy's
`https://` upstream.
## Removing tracing headers
Clients and services often send distributed tracing headers that carry trace ids, sampling
decisions and baggage (sometimes user ids) to whatever server they talk to. To keep them from
leaving through the proxy:
```java
MicroProxy.bootstrap()
.withoutTracingHeadersUpstream() // HttpHeaderNames.TRACING_HEADERS
.plusStrippedRequestHeaders("X-Request-Id") // and any others
.start();
```
- **The list:** `traceparent`, `tracestate`, `baggage`, `b3`, `X-B3-TraceId`, `X-B3-SpanId`,
`X-B3-ParentSpanId`, `X-B3-Sampled`, `X-B3-Flags`, `uber-trace-id`, `X-Amzn-Trace-Id`,
`X-Cloud-Trace-Context`, `grpc-trace-bin` and `sentry-trace`. `X-Request-Id` is not on it,
because servers often want it; add it with `plusStrippedRequestHeaders` if yours do not.
- **When:** right before a request is written to the server or chained proxy, after every filter,
so headers a filter added are removed too. Filters, `HttpLogger`'s "forwarded as" view and
`ActivityTracker.requestReceivedFromClient` still see them; `requestSentToServer` does not.
- **Where:** plain requests, requests inside intercepted (MITM) sessions, WebSocket upgrade
requests, and the `CONNECT` requests sent to HTTP chained proxies. The bytes of a tunnel that is
not intercepted cannot be touched.
- `withStrippedRequestHeaders(names...)` replaces the list instead of adding to it; names are
matched case-insensitively.
- On the command line: `--strip-tracing-headers` and `--strip-request-headers a,b`; in properties
files: `strip_tracing_headers=true` and `strip_request_headers=a,b`.
## Shared server connection pool
By default, as in LittleProxy, server connections are kept per client connection. Calling
`withSharedServerConnectionPool(true)` shares them between all clients:
- **Leasing:** a connection is leased for one request and returned when the response completes
with keep-alive.
- **Pool keys:** idle connections are kept per target and route, so direct, via-proxy and
intercepted connections never mix.
- **Limits:** `withMaxConnections` (default 200) and `withMaxConnectionsPerHost` (default 10).
When the pool is full, a request waits up to the connect timeout for a connection, then gets
`503`. LittleProxy returned `502` immediately.
- **Idle eviction:** `withPoolIdleTimeout` closes connections idle for too long. A pooled
connection the server closed while idle is retried transparently.
- **Intercepted TLS:** `withPoolSharedMitmConnections(true)` lets intercepted sessions take their
upstream TLS connection from the pool and return it when the client leaves.
`withPoolPerRequestInMitm(true)` leases it per request instead. Managers chosen per client
connection only share with clients given the same manager (see
[Interception per client connection](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#interception-per-client-connection)).
- **Metrics:** `HttpProxyServer.getServerConnectionPoolMetrics()` reports counts.
# Filters and failure responses
Filters are Java hooks that see, change, answer or drop each exchange as it passes the proxy, with
LittleProxy's API. This page lists the hooks, shows the lambda builder and the per-request
connection settings (connect timeout, TLS handshake timeout, retry backoff), and explains how the
proxy answers when it cannot reach the server. Start with [Your first
filter](https://mahmoudimus.github.io/MicroProxy/getting-started/first-filter/) for a runnable example.
## The filter API
An `HttpFiltersSource` returns an `HttpFilters` for each request (or `null` to proxy it
unfiltered). The `HttpFilters` overrides the hooks it needs; every method has a default, so the
`*Adapter` classes are only conveniences. Install a source with `withFiltersSource(...)`, and add
more with `plusFiltersSource(...)`: several sources run in order as an `HttpFiltersChain`
(`HttpFiltersChain.of(a, b, c)` builds one by hand).
| Hook | When it runs | What it may do |
|---|---|---|
| `clientToProxyRequest(HttpObject)` | for the request head and each body piece from the client | return a response for the head to answer without contacting the server (a short-circuit), or `null` to go on |
| `proxyToServerRequest(HttpObject)` | after the proxy has rewritten the headers, before the server is resolved and connected (after it is resolved with the [protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) field `resolve_before_proxy_to_server_request`, as in the LittleProxy preset) | the same |
| `serverToProxyResponse(HttpObject)` | for the response head and each body piece from the server | return it (changed or replaced), or `null` to abort and disconnect the client |
| `proxyToClientResponse(HttpObject)` | before each piece is written to the client, also for the proxy's own responses (a `CONNECT`'s `200` passes `serverToProxyResponse` first; with the [protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) field `filter_connect_answer` off, as in the LittleProxy preset, neither hook sees it) | the same |
| `proxyToServerFailure(ProxyFailure)` | when the proxy has to answer itself ([below](https://mahmoudimus.github.io/MicroProxy/guides/filters/#failure-responses)) | return the answer, or `null` for the default |
| `proxyToServerAllowMitm()`, `proxyToServerAllowMitm(ClientHello)` | for a `CONNECT`, and once its ClientHello is read | decline interception ([MITM](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)) |
| `proxyToServerMitmHandshake()` | for a `CONNECT` the proxy intercepts, after authentication and `clientToProxyRequest` | run the server's TLS handshake before answering it (`EAGER`) or after the ClientHello (`DEFERRED`), over the [protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/); `builder().mitmServerHandshake(...)` ([MITM](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-tls-failures)) |
| `proxyToServerTlsIdentity(hostAndPort)` | before a request that reaches its server over TLS chooses a connection | return the server name (SNI) and verification host for it; `builder().tlsIdentity(...)` ([MITM](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-name-and-verification-host)) |
| `proxyToServerResolutionStarted(hostAndPort)` | before the server's name is resolved | return an address to use instead |
| `proxyToServerConnectTimeout()` | when a new server connection is needed | change the connect timeout ([below](https://mahmoudimus.github.io/MicroProxy/guides/filters/#per-request-connect-timeout)) |
| `proxyToServerTlsHandshakeTimeout()` | when a new server connection is needed | change the upstream TLS handshake timeout ([below](https://mahmoudimus.github.io/MicroProxy/guides/filters/#per-request-tls-handshake-timeout)) |
| `chainedProxyRetryBackoff()` | when an attempt failed and another route remains | change the wait before the next chained proxy ([below](https://mahmoudimus.github.io/MicroProxy/guides/filters/#per-request-retry-backoff)) |
| `requestBufferSizeInBytes(request)`, `responseBufferSizeInBytes(response)` | after a message's head | buffer that message whole ([Rewriting bodies](https://mahmoudimus.github.io/MicroProxy/guides/rewriting-bodies/)) |
| `webSocketFrameReceived`, `filterWebSocketFrame`, `filterWebSocketMessage(s)` | after a WebSocket upgrade | watch, rewrite or drop frames and messages ([WebSockets](https://mahmoudimus.github.io/MicroProxy/guides/websockets/)) |
| `proxyToClientResponseSent(response, source)`, `exchangeEnded(completed)` | once the response is written / once the exchange ends however it ended | release what the filter holds per exchange ([Activity tracking](https://mahmoudimus.github.io/MicroProxy/observability/activity-tracking/)) |
The notification hooks (`proxyToServerRequestSending`, `proxyToServerRequestSent`,
`serverToProxyResponseReceiving`, `serverToProxyResponseReceived`,
`serverToProxyResponseTimedOut`, `proxyToServerResolutionSucceeded` / `Failed`,
`proxyToServerConnectionStarted` / `SSLHandshakeStarted` / `Failed` / `Succeeded`) report
progress and return nothing. The Javadoc of [`HttpFilters`](https://mahmoudimus.github.io/MicroProxy/api/org/microproxy/HttpFilters.html) describes each one.
A source can ask for whole messages with `getMaximumRequestBufferSizeInBytes()` /
`getMaximumResponseBufferSizeInBytes()`, as in LittleProxy; otherwise bodies stream through in
pieces. Bodies that no filter inspects skip the message objects entirely (see
[Architecture](https://mahmoudimus.github.io/MicroProxy/architecture/#why-virtual-threads-instead-of-netty)).
### Addresses of a flow
The `FlowContext` a source and its filters get names four addresses of the client connection,
for logging, or for a filter that answers or resolves a request by the address the client came
in on:
| Method | The address |
|---|---|
| `getClientAddress()` | the client: the PROXY protocol header's source when one was accepted, else the TCP peer |
| `getDirectClientAddress()` | the TCP peer: the load balancer that sent the PROXY header, or the client |
| `getLocalAddress()` | the proxy's own address that the client connected to: the TCP socket's local address on every TCP listener (plain, TLS, h2c, SOCKS, transparent, reverse), the UDP address for HTTP/3 and transparent QUIC, the outer connection's inside an intercepted `CONNECT` stream |
| `getProxyProtocolDestinationAddress()` | the PROXY header's destination, the address the client reached at the load balancer; `null` without a header |
`getHttpVersion()` says which protocol the client speaks to the proxy (HTTP/1.1 for HTTP/1.x,
HTTP/2 or HTTP/3), the one whose [protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) applies, so a filter can
branch on it too.
A `FullFlowContext` has the same, plus the server side (`getRemoteAddress()`, the server or
chained proxy connected to). In LittleProxy the local address came from the
`ChannelHandlerContext` (`ctx.channel().localAddress()`) and the PROXY header's destination from
`ClientToProxyConnection.getHaProxyMessage()`.
## Filters from lambdas
`HttpFilters` keeps LittleProxy's shape: a class that overrides the hooks it needs. For small
filters, `HttpFilters.builder()` takes one lambda per hook instead, and `HttpFiltersSource` is a
functional interface:
```java
HttpFilters filters = HttpFilters.builder()
.onRequest(req -> req.uri().contains("/admin") ? forbidden() : null) // short-circuit
.beforeSending(req -> { req.headers().set("X-Trace", traceId()); return null; })
.onResponse(res -> { res.headers().remove("Server"); return res; })
.onWebSocketFrame((frame, fromClient) -> frame.isPing() ? null : frame)
.build();
MicroProxy.bootstrap().withFiltersSource((request, ctx) -> filters).start();
```
Other hooks: `onRequestBody`, `onResponseBody`, `beforeResponding`, `onFailure`, `onWebSocketMessage`,
`resolveWith`, `allowMitm`, `connectTimeout`, `tlsHandshakeTimeout`, `retryBackoff`, `bufferRequests` and
`bufferResponses`. Registering a hook twice runs both in order.
`log(httpLogger)` logs each exchange around the lambdas (see [Request/response
logging](https://mahmoudimus.github.io/MicroProxy/observability/request-logging/)), whether a lambda source returns the built filters, as above,
or they are the source themselves: they are also an `HttpFiltersSource` that returns them for every
request, so `withFiltersSource(filters)` works as well.
### Per-request connect timeout
`HttpFilters.proxyToServerConnectTimeout()` (or `connectTimeout(Duration)`
in the builder) bounds the TCP connect of the request's new connections, direct or to each chained
proxy tried, instead of the server's `withConnectTimeout`. `null` (the default) or a non-positive
duration keeps the server's; in a chain the shortest timeout wins. It is asked only when a new
connection is needed, and does not cover name resolution or TLS handshakes:
```java
HttpFilters.builder()
.connectTimeout(Duration.ofMillis(500)) // fail fast, e.g. to fall back to the next upstream
.build();
```
### Per-request TLS handshake timeout
`HttpFilters.proxyToServerTlsHandshakeTimeout()` (or `tlsHandshakeTimeout(Duration)` in the
builder) bounds each upstream TLS handshake of the request's new connections instead of the server's
`withTlsHandshakeTimeout` (10 s by default): with the server when the proxy speaks TLS to it (an
intercepted `CONNECT`, an absolute `https://` URL, HTTP/2 to the server), and with each TLS chained
proxy tried (`https://` and `h2://` ones), including the server's handshake through a tunnel. `null`
(the default) or a non-positive duration keeps the server's, so a request cannot turn the limit
off; in a chain the shortest timeout wins. A handshake that runs out of time is a
`ProxyFailure.TlsFailed` whose cause is a `SocketTimeoutException` (response flag `UF`), and the
next chained proxy, if any, is tried:
```java
HttpFilters.builder()
.tlsHandshakeTimeout(Duration.ofMillis(200)) // a server that accepts but never handshakes fails fast
.build();
```
Upstream HTTP/3 (QUIC) handshakes keep `Http3Options.upstreamHandshakeTimeout()`: those
connections are shared by every client, and one that is not made in time sends the request over
TCP rather than failing it, where this timeout bounds the TLS handshake.
### Per-request retry backoff
`HttpFilters.chainedProxyRetryBackoff()` (or `retryBackoff(RetryBackoff)` in the builder) chooses
the wait between the request's attempts at its chained proxies instead of the server's
[`withChainedProxyRetryBackoff`](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#retries-and-backoff): `RetryBackoff.exponential`
for exact doubling waits, `exponentialWithJitter`, `fixed`, or `none()` to try the next route at
once. `null` (the default) keeps the server's; in a chain the first filter that names a backoff
wins. It is asked only after an attempt failed, so requests that connect at once never ask:
```java
HttpFilters.builder()
.retryBackoff(RetryBackoff.exponential(Duration.ofMillis(100), Duration.ofSeconds(1))) // 100, 200, 400, 800, 1000 ms
.build();
```
The policy carries the budget for all of the request's waits too: by default the connect timeout,
at most 30 seconds; `.withBudget(Duration)` sets another, and `.unbudgeted()` waits every time the
policy names (see [Retries and backoff](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#retries-and-backoff)).
Bodies and frames are only split into pieces when a body or frame hook is registered; otherwise they
take the fast path, as they do for a filters class that doesn't override those hooks. A filters
class that overrides `clientToProxyRequest` or `serverToProxyResponse` only to read heads can say so
by implementing `SelectiveFilters`: the proxy then asks its `sees(Body)` instead of guessing from the
class.
## Failure responses
When the proxy has to answer a request itself, it sends a short plain-text body (`Bad Gateway`,
`Gateway Timeout`, ...) that never echoes the request. The cause is a sealed `ProxyFailure`:
| `ProxyFailure` | when | default |
|---|---|---|
| `UnresolvedHost` | the server's name did not resolve | `502` |
| `ConnectFailed` | connection refused, unreachable or timed out; a chained proxy refused, or its own name did not resolve | `502` |
| `TlsFailed` | the TLS handshake with the server (MITM, or [TLS origination](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-origination-https-uris-without-connect)) or a TLS chained proxy failed or timed out | `502` |
| `ServerTimeout` | no response within the idle timeout | `504` |
| `BadServerResponse` | malformed response, or the server closed or failed before the head was complete | `502` |
| `NoRoute` | the request names no host, or the chained proxy manager offered no route | `502` |
| `NoConnectionAvailable` | the shared connection pool had no connection to spare | `503` |
| `BadRequest` | an origin-form request (unless allowed), a client's `https://` request with TLS origination off, or an invalid `CONNECT` target | `400` |
| `RequestTooLarge` | the body exceeds what the filters asked to buffer | `413` |
Filters can answer first (`HttpFilters.proxyToServerFailure`, or `onFailure` in the builder; in a
chain the first answer wins), then a `FailureResponder`; returning `null` keeps the default:
```java
MicroProxy.bootstrap()
.withFailureResponder((request, failure) -> switch (failure) {
case ProxyFailure.TlsFailed f -> errorPage(502, "The site's certificate is not trusted");
case ProxyFailure.ServerTimeout f -> errorPage(504, "The site took too long to answer");
default -> null;
})
.start();
```
Starlark scripts answer through `on_failure` (see [Scripting with Starlark](https://mahmoudimus.github.io/MicroProxy/guides/scripting/)).
The proxy frames whatever is returned (`Content-Length`, keep-alive, no body for `HEAD`), and it
passes `proxyToClientResponse` like the default answers. A responder that throws is logged and the
default is sent. Requests the proxy cannot parse at all are answered with a plain `4xx` without
asking anyone. `HttpCache` uses the filter hook to serve stale entries when servers are
unreachable.
Customising the proxy's own error pages belongs here, in `proxyToServerFailure` or a
`FailureResponder`, which get the failure itself, rather than in `proxyToClientResponse`.
### The proxy's responses in proxyToClientResponse
`proxyToClientResponse` sees the server's responses, and also the responses the proxy answers with
itself: its failure answers above (a `FailureResponder`'s included) and the short-circuit
responses filters returned from `clientToProxyRequest` or `proxyToServerRequest` (the
`HttpCache`'s answers too). No filter sees the `407` authentication challenge, which is sent
before the request's filters are made. What the hook may do with the proxy's responses is the
[protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) field `generated_responses`:
| `GeneratedResponses` | `proxyToClientResponse` | For |
|---|---|---|
| `REPLACEABLE` (the default; LittleProxy 2.x) | sees them, may return another response or `null` (close without an answer) | filters that brand every response: their own error pages, `503` with `Retry-After` in place of `502`, CORS or request-id headers on every response |
| `IN_PLACE` (LittleProxy 1.x and its forks) | sees them, may change them in place or return `null`; another response it returns is ignored | filters written for the server's responses, which must not turn the proxy's own errors into something else (a filter that answers some responses `400` would otherwise turn an unreachable server's `502` into `400`) |
| `SKIP` | never sees them; trackers and `proxyToClientResponseSent` still do | filters that must only ever see the server's responses |
```java
MicroProxy.bootstrap()
.onHttp1(p -> p.generatedResponses(GeneratedResponses.IN_PLACE))
.start();
```
Inside the hooks, `FlowContext.responseSource()` says where the response at hand came from:
`SERVER`, `PROXY` (the proxy's answers, and the `200` that opens a `CONNECT` tunnel), `FILTER`
(a short-circuit response, or one a filter replaced or restatused in `serverToProxyResponse`),
`CACHE` or `REPLAY`; once the response is sent, where the response the client got came from, as
`proxyToClientResponseSent` is told.
# Protocol policies
Where MicroProxy can behave in more than one way (the order of the filter hooks, how the answer to
a `CONNECT` is written, how the proxy talks TLS to servers), a protocol policy chooses, separately
for HTTP/1.x, HTTP/2 and HTTP/3 clients. Use this page to give some clients LittleProxy's
behaviour while others keep MicroProxy's, or to change one such behaviour on its own.
## The fields of a policy
| Field (Java / configuration name) | MicroProxy's default | LittleProxy's (`ProtocolPolicy.littleProxy()`) |
|---|---|---|
| `resolveBeforeProxyToServerRequest` / `resolve_before_proxy_to_server_request`: whether the server's name is resolved before `proxyToServerRequest` runs (a name that does not resolve is then answered `502` without that hook) | `false`: the hook can answer or redirect a request without a DNS lookup | `true` |
| `filterConnectAnswer` / `filter_connect_answer`: whether the `200 Connection established` answer to a client's `CONNECT` goes through `serverToProxyResponse` and `proxyToClientResponse` (trackers, `proxyToClientResponseSent` and `exchangeEnded` see it either way) | `true`: a filter can see it or turn it into a refusal | `false`: written straight to the client |
| `servePlainHttpAfterConnect` / `serve_plain_http_after_connect`: whether plain HTTP sent after a `CONNECT` that could be intercepted (a `ws://` WebSocket, say) is served as HTTP, through the filters | `true` | `false`: relayed as raw bytes |
| `proxyProtocolDestination` / `proxy_protocol_destination`: the destination a PROXY header sent to a server names, when the client brought none ([PROXY protocol](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#proxy-protocol)) | `LOCAL_ADDRESS` (`local_address`): the address the client connected to | `SERVER_ADDRESS` (`server_address`): the server connection's remote address, and no header when the families differ |
| `generatedResponses` / `generated_responses`: what `proxyToClientResponse` may do with the responses the proxy answers with itself, its error responses and filters' short-circuit responses ([The proxy's responses in proxyToClientResponse](https://mahmoudimus.github.io/MicroProxy/guides/filters/#the-proxys-responses-in-proxytoclientresponse)) | `REPLACEABLE` (`replaceable`): replace them | `REPLACEABLE`, as LittleProxy 2.x; LittleProxy 1.x and its forks: `IN_PLACE` (`in_place`), change them in place only. `SKIP` (`skip`) keeps them from the hook |
| `mitmServerHandshake` / `mitm_server_handshake`: when the proxy runs its TLS handshake with the server for an intercepted `CONNECT` ([Server TLS failures](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-tls-failures)); `HttpFilters.proxyToServerMitmHandshake()` overrides it per request | `DEFERRED` (`deferred`): after the `CONNECT` is answered and the ClientHello read, mirroring it; a failure answers the session's first request `502` | `EAGER` (`eager`): before answering, so a failure answers the `CONNECT` `502` |
| `tlsRetryWithoutSni` / `tls_retry_without_sni`: whether a server that rejects the server name with an `unrecognized_name` alert is tried once more without SNI ([Server TLS failures](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-tls-failures)) | `false` | `true` |
| HTTP/1 request-head deadline: `requestHeadTimeout` / `request_head_timeout`, measured from its first byte to the end of its headers; zero disables it | 30 seconds: an incomplete head receives `408` and the connection closes | 30 seconds (MicroProxy hardening; LittleProxy has no corresponding setting) |
| HTTP/1 only: `allowChunkedLength` / `allow_chunked_length`: whether a request with both `Transfer-Encoding` and `Content-Length` is accepted ([Security](https://mahmoudimus.github.io/MicroProxy/security/#request-parsing)) | `withAllowChunkedLength` (`false`) | not part of the preset: LittleProxy refuses such requests too |
Each field and the LittleProxy 2.9.1 code that behaves so are listed in
[Migrating from LittleProxy](https://mahmoudimus.github.io/MicroProxy/migrating-from-littleproxy/#behaviour-differences).
## Configuring policies
On the bootstrap, `onHttp1`, `onHttp2` and `onHttp3` set fields of one protocol's policy, and
`onAllProtocols` of every protocol's. Each takes a lambda that sets fields on a builder; `preset`
copies every field of a preset, and the fields set after it win over it:
```java
MicroProxy.bootstrap()
.onHttp1(p -> p.preset(ProtocolPolicy.littleProxy()) // LittleProxy's behaviour for HTTP/1 clients
.filterConnectAnswer(true)) // ... but filters see the CONNECT's 200
.onAllProtocols(p -> p.tlsRetryWithoutSni(true))
.start();
```
`withLittleProxyCompatibility()` (`--littleproxy-compat`, `littleproxy_compatibility=true`) makes
the LittleProxy preset HTTP/1's defaults; HTTP/2 and HTTP/3 clients, which LittleProxy never
served, keep MicroProxy's. `allowChunkedLength` is only available on HTTP/1's builder
(`Http1PolicyBuilder`); `requestHeadTimeout` is available in every policy but only applies to
HTTP/1.
Set the timeout for every protocol with `withRequestHeadTimeout(Duration)` or
`--request-head-timeout` / `request_head_timeout`. The HTTP/1 policy can override it with
`onHttp1(p -> p.requestHeadTimeout(Duration))`, `--http1-policy request_head_timeout=` or
`policy.http1.request_head_timeout=`. The default is 30 seconds; zero disables the deadline.
HTTP/2 and HTTP/3 do not use this field.
In a properties file, `policy.` sets a field for every protocol and `policy.http1.`,
`policy.http2.` and `policy.http3.` for one; `preset` names a preset (`littleproxy`
or `microproxy`) and is applied before the other fields of its scope. On the command line,
`--policy`, `--http1-policy`, `--http2-policy` and `--http3-policy` take comma-separated
`field=value` pairs, applied left to right:
```properties
policy.http1.preset=littleproxy
policy.http1.filter_connect_answer=true
policy.tls_retry_without_sni=true
```
```bash
java -jar microproxy.jar --mitm --http1-policy preset=littleproxy,filter_connect_answer=true \
--policy tls_retry_without_sni=true
```
## Which policy applies
A field's value for an exchange is, from the first that sets it:
1. the request's filters, for a field a hook decides per request (`mitm_server_handshake`:
`proxyToServerMitmHandshake()`);
2. the policy of the client's protocol (`onHttp1`, `policy.http1.*`, ...);
3. the policy of every protocol (`onAllProtocols`, `policy.*`, and the bootstrap's own setters
for those fields, such as `withTlsRetryWithoutSni`);
4. the protocol's defaults: `ProtocolPolicy.defaults()`, or for HTTP/1 with
`withLittleProxyCompatibility()`, `ProtocolPolicy.littleProxy()`.
The client's protocol is the one it speaks to the proxy, `FlowContext.getHttpVersion()` (and
`ctx.protocol` in Starlark), whatever the proxy speaks to the server:
- An HTTP/1.x connection's requests, its `CONNECT`s included, take HTTP/1's policy; an HTTP/2
connection's streams (`h2` and `h2c`, their `CONNECT`s and extended `CONNECT`s included) take
HTTP/2's; an HTTP/3 connection's streams take HTTP/3's.
- Inside a tunnel the proxy intercepts, the requests take the policy of what the client speaks
inside it. The `CONNECT` stream of an HTTP/2 client is HTTP/2's (its answer, and what is done
with plain HTTP after it), while the HTTP/1.1 requests the client then sends inside the stream's
TLS are HTTP/1's.
- SOCKS clients and transparent TLS connections, whose `CONNECT` the proxy makes up, are HTTP/1
connections until the intercepted TLS negotiates HTTP/2. A reverse proxy's clients take the
policy of the protocol they speak to it. Transparent QUIC, and the QUIC inside a CONNECT-UDP
flow, is HTTP/3.
`HttpProxyServer.getProtocolPolicy(HttpVersion)` returns a protocol's policy with every field
resolved, and the admin [`/config_dump`](https://mahmoudimus.github.io/MicroProxy/reference/admin-api/) shows them under
`protocol_policies`. The policies are resolved once, when the server starts: choosing one costs an
exchange nothing.
# MITM, certificates and TLS
Intercept HTTPS: the proxy terminates the client's TLS with a certificate its own CA issues, and
opens its own TLS connection to the server, so filters, scripts and logs see the decrypted
requests. This page covers the CA, choosing a CA and upstream trust per client, how the ClientHello
decides what is intercepted, host rules, ALPN mirroring and the TLS versions the proxy allows. Use
it whenever you pass `--mitm` or `withManInTheMiddle(...)`.
## Turning interception on
From the command line, `--mitm` creates (or reuses) a CA in `microproxy-ca.p12` and writes its
certificate to `microproxy-ca.pem`. To intercept HTTPS without errors, add that certificate to
your client's trust store.
```bash
java -jar microproxy.jar --port 8080 --mitm
curl -x http://127.0.0.1:8080 --cacert microproxy-ca.pem https://example.com/
```
| Flag | Meaning |
|---|---|
| `--mitm` | intercept HTTPS with a generated CA |
| `--mitm-ca ` | the CA key store (created if missing; default `./microproxy-ca.p12`); its certificate is written next to it as `.pem` |
| `--mitm-ca-password ` | the key store's password (default `microproxy`) |
| `--mitm-trust-all` | do not validate upstream server certificates |
From Java, give the bootstrap a `MitmManager`. `CertificateAuthorityMitmManager` issues per-host
certificates on demand (EC P-256), whose SANs copy the real server's DNS names, through a small
built-in X.509/DER encoder (`org.microproxy.tls.CertificateBuilder`): only the JDK is used. It
validates real servers against the JDK trust store unless given another upstream context:
```java
CertificateAuthority ca = CertificateAuthority.loadOrCreate(
Path.of("microproxy-ca.p12"), "microproxy".toCharArray(), "MicroProxy CA");
ca.writeCertificatePem(Path.of("microproxy-ca.pem")); // for clients to trust
MicroProxy.bootstrap()
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca)) // JDK trust store
// .withManInTheMiddle(new CertificateAuthorityMitmManager(ca, SslContexts.trustAll())) // --mitm-trust-all
.start();
```
`CertificateAuthority.generate(commonName)` makes a CA in memory. For servers signed by a
private CA, `SslContexts.systemDefaultPlus(caCert)` trusts the JDK's roots plus extra anchors
([Upstream proxies and NO_PROXY](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#upstream-proxies-and-no_proxy)), and
`SslContexts.trusting(anchors...)` only the given ones. Hosts that must never be intercepted
(banks, pinned apps) are left alone with [host rules](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#ignoring-and-allowing-hosts) or a filter.
## Interception per client connection
The proxy hands every `MitmManager` call the client connection's `FlowContext`, with its
authenticated user (`getClientDetails().getUserName()`) and address (`getClientAddress()`). Two
ways to use it:
- **One manager per tenant:** `MitmManager.perConnection(flow -> ...)` picks a manager when a
connection is first intercepted and keeps it for that connection. Return the same instance for
clients that may share server connections; `null` tunnels without interception.
```java
Map byTenant = Map.of(
"alice", new CertificateAuthorityMitmManager(aliceCa, aliceUpstreamContext),
"bob", new CertificateAuthorityMitmManager(bobCa, bobUpstreamContext));
MicroProxy.bootstrap()
.withProxyAuthenticator(authenticator)
.withManInTheMiddle(MitmManager.perConnection(
flow -> byTenant.get(flow.getClientDetails().getUserName())))
.start();
```
- **One manager deciding per call:** override the `FlowContext` overloads,
`serverSslContext(host, port, flow)` (upstream trust store and client certificate; `flow` is a
`FullFlowContext` naming the server and route), `clientSslContextFor(connect, session, flow)`
(the certificate shown to the client) or `configureServerSocket(socket, flow)`. They default to
the methods without `FlowContext`, so existing managers work unchanged. `shouldIntercept(hello,
flow)` decides by the client's ClientHello whether to intercept at all (see
[below](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)).
An upstream context with a key (`SslContexts.withKey(key, chain, trustManagers)`) presents that
client certificate to servers that ask for one.
With `withPoolSharedMitmConnections(true)`, a pooled TLS connection carries the trust and client
certificate it was made with, so the pool keeps them apart. Connections made by a manager chosen
with `forConnection` (which `perConnection` uses) are pooled under that manager and only reused by
clients given the same one. A manager overriding `serverSslContext(host, port, flow)` or
`configureServerSocket(socket, flow)` may decide differently for every client, so its server
connections are not pooled: each client connection keeps its own. Overriding only
`clientSslContextFor(..., flow)` does not affect pooling.
## Deciding what to intercept: the ClientHello
With a MITM manager, a `CONNECT` is not intercepted blindly. The proxy connects to the server (TCP,
and through any chained proxy), answers `200`, and reads what the client sends first without
consuming it, as mitmproxy does:
- **A TLS ClientHello** is read across as many TLS records as it spans (post-quantum key shares
make it several), up to 64 KiB and within the TLS handshake timeout, and parsed into a
`ClientHello`: the server name (SNI), the ALPN protocols, the TLS versions and the cipher
suites, GREASE values included. The rules below decide whether to intercept. If they do, the
proxy shakes hands with the server first and then with the client, replaying the bytes it read
into the JDK's handshake with `SSLSocketFactory.createSocket(socket, consumed, autoClose)`. If
not, the bytes are relayed to the server first and the connection is a plain tunnel, so the
client sees the real server's certificate.
- **Plain HTTP** (a request line, as a `ws://` WebSocket through a proxy sends) is served as
HTTP: its requests go to the `CONNECT` target through the filters, as in an intercepted session
but without TLS. Absolute-form requests and `CONNECT`s in it are proxy requests for that target,
which the client takes for its HTTP proxy ([In front of an HTTP or HTTPS
proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy)). With the
[protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) field `serve_plain_http_after_connect` off (as in the
LittleProxy preset, `--littleproxy-compat` for HTTP/1 clients) it is tunnelled, as LittleProxy
does.
- **Anything else** is tunnelled, the bytes read so far first: other protocols, HTTP/2 with prior
knowledge, and a malformed or oversized ClientHello. A server that speaks first (SMTP, SSH) is
noticed while the proxy waits for the client, and its connection is tunnelled as well.
So it goes, too, for a tunnel that a client asks its own proxy for inside a session MicroProxy
serves, once that proxy has opened it: everything then reaches the target through that proxy's
tunnels ([In front of an HTTP or HTTPS proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy)).
Interception is decided in this order; the first "no" tunnels the connection untouched:
1. `HttpFilters.proxyToServerAllowMitm()` and the [ignore rules](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#ignoring-and-allowing-hosts)
against the `CONNECT` target, when the `CONNECT` arrives. Nothing is read from the client
then, as before.
2. With the ClientHello: the host rules against the `CONNECT` target and the SNI; a client whose
ALPN offers no protocol the proxy speaks (only `acme-tls/1`, say; with a
[TCP interceptor](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/#raw-tcp-streams), such clients are intercepted, except `acme-tls/1`, and
their sessions relayed as raw TCP); then `MitmManager.shouldIntercept(clientHello, flow)` and
`HttpFilters.proxyToServerAllowMitm(clientHello)`.
```java
MicroProxy.bootstrap()
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca))
.withIgnoreHosts("\\.bank\\.example:443$") // --ignore-hosts, ignore_hosts
.withFiltersSource((request, flow) -> HttpFilters.builder()
.allowMitmFor(hello -> !"pinned.example.com".equals(hello.sni()))
.build())
.start();
```
`FlowContext.getClientHello()` then returns the ClientHello to the session's requests, and to
`MitmManager.clientSslContextFor(connect, session, flow)`. Scripts get `on_client_hello` and
`ctx.client_hello` (see [Scripting](https://mahmoudimus.github.io/MicroProxy/guides/scripting/)).
The same applies to a `CONNECT` stream of an HTTP/2 client ([CONNECT and
WebSockets](https://mahmoudimus.github.io/MicroProxy/guides/http2/#connect-and-websockets)). A `CONNECT` inside an intercepted session, or inside a plain
HTTP one, goes to the session's server instead ([In front of an HTTP or HTTPS
proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy)): the tunnel it opens is intercepted without reading
its ClientHello, unless the host rules ignore its target.
### Server TLS failures
When the proxy runs its handshake with the server is the [protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/)
field `mitm_server_handshake` (`MitmServerHandshake`), which a request's filters can override
(`HttpFilters.proxyToServerMitmHandshake()`, `HttpFilters.builder().mitmServerHandshake(...)`),
after proxy authentication and `clientToProxyRequest` on the `CONNECT`, so the choice can depend
on the authenticated user:
| `MitmServerHandshake` | The server's handshake | When every route's handshake fails |
|---|---|---|
| `DEFERRED` (the default) | after the `CONNECT` is answered and the client's ClientHello read, mirroring its ALPN protocols (and its SNI for a `CONNECT` to an IP address); the ClientHello also decides first whether to intercept at all | the client's handshake completes with a certificate made from the host name, and its first request in the session is answered `502` (`ProxyFailure.TlsFailed`, through `proxyToServerFailure` and the `FailureResponder`), as mitmproxy answers; a client whose handshake cannot complete is disconnected |
| `EAGER` (LittleProxy's) | before the `CONNECT` is answered, sending the `CONNECT`'s host (or the request's `TlsServerIdentity`) as SNI and offering the proxy's own ALPN protocols (`h2` and `http/1.1` with HTTP/2 to servers), since the ClientHello has not come yet | the `CONNECT` is answered `502` (`ProxyFailure.TlsFailed`), and no tunnel opens |
Either way a failed handshake is a failed attempt at its route: the next chained proxy (or the
direct fallback) the `ChainedProxyManager` offered is tried, after the
[backoff](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#retries-and-backoff) wait, as when the connection itself fails;
only the last failure is answered, with `UF` and, after several routes, `URX` among the
[response flags](https://mahmoudimus.github.io/MicroProxy/reference/response-flags/#response-flags-and-details). The retry without
SNI below and the request's handshake timeout (`proxyToServerTlsHandshakeTimeout`) apply to
each attempt.
`EAGER` trades the mirroring away. The server's handshake offers the proxy's own protocols: when
the server chooses HTTP/2 and the client then offers only HTTP/1.1, the client's requests go on a
second connection that offers what it offered (the first is kept for HTTP/2 requests). A
server is never offered the protocols of a client that speaks no HTTP, nor the SNI of a client
that `CONNECT`s to an IP address, and a server that speaks no TLS and never answers the proxy's
ClientHello holds the `CONNECT` until the TLS handshake timeout, then answers it `502` (as in
LittleProxy). A session that the
ClientHello then decides not to intercept (host rules, `MitmManager.shouldIntercept`,
`proxyToServerAllowMitm(ClientHello)`) is tunnelled over a new connection; the one made for the
handshake is closed. LittleProxy runs the handshake first because its
`MitmManager.clientSslEngineFor(HttpRequest, SSLSession)` makes the client's certificate from the
server's session; proxy authentication happens on the `CONNECT` in both modes. Transparent TLS
connections, whose ClientHello the proxy has read already, are always deferred.
A server that turns out not to speak TLS gets the client's bytes tunnelled, in both modes.
Unreachable servers fail the `CONNECT` with `502` in both, since it is connected before it is
answered. With `DEFERRED`, `proxyToServerConnectionSSLHandshakeStarted` comes after the
`CONNECT`'s `200`, and the `CONNECT` exchange completes (`proxyToClientResponseSent`,
`responseCompleted`) once the server handshake is over, so its timings include that handshake.
Every server TLS failure reaches filters, the `FailureResponder` and the access log as a
`ProxyFailure.TlsFailed` whose `cause()` is the JDK's own exception with its whole cause chain:
for a revoked certificate, the `CertPathValidatorException` with reason `REVOKED` is in it, for
an expired one the `CertificateExpiredException`. `TlsFailed.reason()` reads the chain:
| `reason()` | When |
|---|---|
| `CERTIFICATE_REVOKED`, `REVOCATION_STATUS_UNKNOWN` | revocation checking (a `PKIXRevocationChecker` in the trust manager's parameters) found the certificate revoked, or could not tell |
| `CERTIFICATE_EXPIRED`, `CERTIFICATE_NOT_YET_VALID` | a certificate of the chain is out of its validity period |
| `CERTIFICATE_UNTRUSTED` | no path to a trusted anchor, or another validation check failed |
| `HOSTNAME_MISMATCH` | the certificate is trusted but not valid for the host it was checked against |
| `ALERT_RECEIVED` | the server ended the handshake with an alert; `alert()` names it (`handshake_failure`, `unrecognized_name`, ...) |
| `TIMEOUT` | the handshake overran `--tls-handshake-timeout` |
| `OTHER` | anything else (no common version or cipher suite, the connection closed, ...) |
The access log's `%UPSTREAM_TRANSPORT_FAILURE_REASON%` is that reason's name and the innermost
cause's message: `certificate_revoked: Certificate has been revoked, reason: ...`.
Some servers answer a server name (SNI) they do not recognize with a fatal `unrecognized_name`
alert (RFC 6066 section 3), even for names they serve. `withTlsRetryWithoutSni(true)`
(`--tls-retry-without-sni`, `tls_retry_without_sni=true`) tries such a server once more, on a new
connection, without SNI, as the JDK's `HttpsURLConnection` once did and LittleProxy forks still
do. It is off by default, so that the misconfiguration shows (browsers and the JDK's `HttpClient`
do not retry), and on by default for HTTP/1 clients with `withLittleProxyCompatibility()`. It is
a [protocol policy](https://mahmoudimus.github.io/MicroProxy/guides/protocol-policies/) field, `tls_retry_without_sni`, so it can be set for
HTTP/1, HTTP/2 or HTTP/3 clients alone too. The retry leaves out the
SNI extension only: the certificate is validated with the same trust and checked against the same
host, so leaving out a name that authenticates nothing weakens no check; a server that then
presents a certificate for another name still fails, with `HOSTNAME_MISMATCH`. Each retry is
logged at `INFO` and counted in the cluster's `ssl.retry_without_sni` (the rejected handshake in
its `ssl.connection_error`). Nothing is remembered: each new connection to such a server pays for
the extra handshake. It applies to servers (intercepted sessions, TLS origination, a reverse
proxy's `https://` upstream), not to TLS chained proxies, and not to HTTP/3.
### Server name and verification host
The proxy sends the request's host as the server name (SNI) and checks the server's certificate
against it; for a `CONNECT` to an IP address it sends the server name the client's ClientHello
named. A route that dials somewhere else than the name the service is known by, an IP address
or an internal load balancer (`proxyToServerResolutionStarted`, a resolver, a reverse proxy's
upstream, a chained proxy), can name both itself: `HttpFilters.proxyToServerTlsIdentity` returns
a `TlsServerIdentity` with the server name and the verification host, `null` keeping the default.
```java
HttpFilters filters = HttpFilters.builder()
.resolveWith(hostAndPort -> hostAndPort.startsWith("api.example.com:")
? new InetSocketAddress("10.0.0.7", 443) : null)
.tlsIdentity(hostAndPort -> hostAndPort.startsWith("api.example.com:")
? TlsServerIdentity.of("api.example.com") : null)
.build();
```
`TlsServerIdentity.of(name)` sends `name` and checks the certificate against it;
`TlsServerIdentity.of(name, verificationHost)` checks it against another host (or IP address)
than the one sent; `TlsServerIdentity.of(null, verificationHost)` keeps the default server name.
The certificate is validated with the MITM manager's trust as always; only the names change. When
the two names differ, the JDK's own check would accept a certificate valid for either, so the
proxy checks the verification host itself, right after the handshake and before the connection
is used, against the certificate's subject alternative names (DNS names, or IP addresses for an
IP address; the common name is not consulted). A context that trusts every server
(`--mitm-trust-all`) checks no names, as before.
The hook is asked for every request that reaches its server over TLS, before a connection is
chosen, and for an intercepted `CONNECT` before the proxy's handshake with its target, with the
server's `host:port`. The identity is part of what keeps server connections apart: one made with
an identity serves only requests with the same identity, never reused, pooled or coalesced
([HTTP/2 coalescing](https://mahmoudimus.github.io/MicroProxy/guides/http2/#connection-coalescing), RFC 9113 section 9.1.1) for another, and requests
with an identity do not go over HTTP/3. The Starlark scripting layer has no hook for it yet.
### Ignoring and allowing hosts
`withIgnoreHosts(...)` (`--ignore-hosts`, `ignore_hosts`) and `withAllowHosts(...)`
(`--allow-hosts`, `allow_hosts`) work as mitmproxy's options of the same names. Each pattern is a
regular expression searched for (not matched whole), ignoring case, in the connection's `host:port`
names: the `CONNECT` target, and the client's SNI with the same port. An ignored connection is
tunnelled untouched. With allow patterns, only connections that one of them finds are
intercepted, and the ignore patterns still apply to those.
| Pattern | Ignores |
|---|---|
| `example\.com` | `example.com:443`, `www.example.com:443`, `example.com.evil.test:443` |
| `^(.+\.)?example\.com:443$` | `example.com` and its subdomains, on port 443 only |
| `^10\.` | `CONNECT`s to `10.x.x.x` addresses, whatever their SNI |
On the command line each flag takes one pattern and may be repeated; in a properties file the
patterns are separated by commas (commas inside `{...}` belong to the pattern). A `CONNECT` whose
target an ignore pattern finds is tunnelled without reading its ClientHello at all. The rules
apply to TLS and QUIC only: plain HTTP requests are proxied as usual. For QUIC the names are the
server and the SNI on the server's port: [transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic)'s flows, and, with
[`--masque-intercept-quic`](https://mahmoudimus.github.io/MicroProxy/guides/masque/#quic-inside-connect-udp-flows), the QUIC inside CONNECT-UDP flows, an
ignored one being relayed as any UDP flow is.
### ALPN mirroring
When intercepting, the server is offered only the ALPN protocols the client offered, of those the
proxy speaks and has enabled: `http/1.1`, `http/1.0`, and `h2` only with
[HTTP/2 to servers](https://mahmoudimus.github.io/MicroProxy/guides/http2/#http2-to-servers) on. The client's own handshake then mirrors the server's
choice when that choice tells what the server speaks, that is, when the server was offered `h2`.
| Client offers | Server offered | Server picks | Client gets |
|---|---|---|---|
| `http/1.1` | `http/1.1` | `http/1.1` | `http/1.1`: never HTTP/2 upstream |
| `h2`, `http/1.1` | `h2`, `http/1.1` | `h2` | `h2` (unless `--no-http2`): HTTP/2 end to end |
| `h2`, `http/1.1` | `h2`, `http/1.1` | `http/1.1` | `http/1.1`, the same as the server |
| `h2`, `http/1.1` (with `--no-http2-upstream`) | `http/1.1` | `http/1.1` | `h2` unless `--no-http2`: the proxy translates |
| no ALPN | `h2`, `http/1.1`, or none with `--no-http2-upstream` | either | no protocol (HTTP/1.1) |
This follows mitmproxy's order (`tlsconfig.py`: the server's handshake first, with the client's
offers, then the client's with the server's choice). Two differences: a client that sends no ALPN
keeps the proxy's old offer, so HTTP/1.1 clients without ALPN (such as the JDK's `HttpClient`)
still reach HTTP/2 servers; and where the server was not offered `h2`,
clients still get HTTP/2 unless `--no-http2` turns it off. Without a server connection of its own, the
session mirrors nothing: an existing HTTP/2 connection the session shares (a client that did not
offer `h2` gets none of it, and its requests connect with its own offers), a reused pooled
connection, or offline interception. The session's requests that connect later offer the same
protocols.
## TLS protocol versions
Every TLS socket the proxy creates allows only `TLSv1.3` and `TLSv1.2` by default: the TLS
listener (`withSslContextSource`), both sides of an intercepted session (towards the client and
towards the server) and connections to TLS chained proxies. `withTlsProtocols(...)` (or
`tls_protocols=...`, `--tls-protocols ...`) changes the list:
```java
MicroProxy.bootstrap().withTlsProtocols("TLSv1.3").start(); // TLS 1.3 only
MicroProxy.bootstrap().withTlsProtocols().start(); // each SSLContext's own defaults
```
- **Per socket:** each socket enables the listed versions its `SSLContext` supports, in the order
given. If it supports none of them, the handshake fails with an error naming both lists (a
`TlsFailed` towards servers, a failed handshake towards clients).
- **Order:** the versions are set right after the socket is created, before
`SslContextSource.configure`, `MitmManager.configureServerSocket` and `ChainedProxy.configure`
run. A hook that sets its own protocols therefore wins.
- **The JDK still applies** `jdk.tls.disabledAlgorithms`, which turns off TLS 1.1 and older,
whatever is listed here.
- **Resumption over TCP is the JDK's.** TLS over TCP (HTTPS, HTTP/2, intercepted sessions, TLS
chained proxies) runs on the JDK's `SSLEngine`, which resumes sessions on its own: each
`SSLContext` caches sessions (`SSLSessionContext`, `javax.net.ssl.sessionCacheSize`,
`jdk.tls.server.enableSessionTicketExtension` for stateless TLS 1.3 tickets) and offers them
when it connects to the same server again. The proxy adds nothing to it, and the JDK sends and
accepts no 0-RTT data. QUIC's TLS is the `quic` module's own, with its own tickets and 0-RTT
([Session resumption and 0-RTT](https://mahmoudimus.github.io/MicroProxy/guides/http3/#session-resumption-and-0-rtt)).
- A server that refuses the proxy's versions with a `protocol_version` alert gets the requests
inside the session a `502` (`TlsFailed`; see [server TLS
failures](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)). One that just closes the connection on
the proxy's `ClientHello` looks like a server that does not speak TLS, and the client's bytes
are tunnelled without interception, as for any non-TLS server.
# Proxy authentication
Make clients authenticate to the proxy with Basic credentials, Bearer tokens or any scheme of your
own, per connection or per request. Use this page to protect a proxy that other machines can reach;
SOCKS5 clients authenticate the same way ([SOCKS](https://mahmoudimus.github.io/MicroProxy/guides/socks/#authentication)), and a Starlark script
can be the authenticator ([Authenticating clients](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#authenticating-clients)).
`withProxyAuthenticator` makes clients authenticate. A `ProxyAuthenticator` that only implements
`authenticate(user, password)` checks `Proxy-Authorization: Basic` and answers failures with
`407` and `Proxy-Authenticate: Basic realm="..."` (`getRealm()`). For other schemes, override
`authenticate(HttpRequest, FlowContext)`, which sees the whole request and the client connection
and returns an `AuthResult`:
- `AuthResult.accept(user)`: the request proceeds, and `user` (which may be `null`) becomes
`ClientDetails.getUserName()` for filters, trackers, access logs, the `ChainedProxyManager`,
MITM decisions and Starlark's `ctx.user`.
- `AuthResult.reject(response)`: the client gets `response`, for instance a `407` with your own
`Proxy-Authenticate` header and body, or a `403`. `AuthResult.reject()` sends the default
Basic `407`.
```java
MicroProxy.bootstrap()
.withProxyAuthenticator(new ProxyAuthenticator() {
@Override
public AuthResult authenticate(HttpRequest request, FlowContext flow) {
String value = request.headers().get(HttpHeaderNames.PROXY_AUTHORIZATION);
if (value != null && value.startsWith("Bearer ")) {
String user = tokens.userFor(value.substring(7)); // your token check
if (user != null) return AuthResult.accept(user);
}
FullHttpResponse challenge = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED, "{\"error\":\"invalid_token\"}");
challenge.headers().set(HttpHeaderNames.PROXY_AUTHENTICATE, "Bearer realm=\"proxy\"");
return AuthResult.reject(challenge);
}
@Override
public boolean authenticate(String userName, String password) {
return false; // no Basic credentials
}
@Override
public boolean authenticateEveryRequest() {
return true; // tokens expire
}
})
.start();
```
- **Once per connection:** by default the first accepted request authenticates its client
connection, and later requests on it are not checked. `authenticateEveryRequest()` checks every
request instead. When the accepted user changes on a connection, the server connections made
for the previous user are given up, so routing is decided again.
- **CONNECT and interception:** a `CONNECT` is authenticated like any request. The requests inside
an intercepted session carry no proxy credentials and are covered by their `CONNECT`, even with
`authenticateEveryRequest()`.
- **Credentials stay here:** whatever the scheme, `Proxy-Authorization` is removed from every
request before filters see it, so it is never forwarded, even by a transparent proxy.
- To accept Basic as well, fall back to `ProxyAuthenticator.super.authenticate(request, flow)`.
- A Starlark script can be the authenticator, with an `authenticate` hook (see
[Scripting with Starlark](https://mahmoudimus.github.io/MicroProxy/guides/scripting/)).
# Concurrency limiting
Cap how many exchanges run at once per client, user, target host or any key, with a bounded wait
queue, `429` answers, a shadow mode and metrics. Use this page to keep one client from monopolising
the proxy or a server; `--max-concurrent-per-client` turns on the common case from the command
line.
`org.microproxy.extras.ConcurrencyLimiter` is a filters source that caps how many exchanges run at
once per key. Requests over the limit wait in a bounded queue, or are answered with `429 Too Many
Requests` (plain text, `Retry-After: 1` by default):
```java
ConcurrencyLimiter limiter = ConcurrencyLimiter.builder()
.key(ConcurrencyLimiter.byUser()) // default: byClientIp(); also byTargetHost()
.permits(8) // per key
.permits(user -> user.equals("batch") ? 32 : null) // null: the default
.queue(16, Duration.ofSeconds(2)) // default: refuse at once
.onReject((key, request, flow) -> metrics.increment("limited", key))
.build();
MicroProxy.bootstrap().withFiltersSource(HttpFiltersChain.of(limiter, myFilters)).start();
```
- **Key:** any `(request, flow) -> String`, for example `flow.getClientDetails().getUserName()` or
the target host; a `null` key leaves the request unlimited.
- **When a permit is held:** it is taken in `clientToProxyRequest`, which runs after proxy
authentication (requests answered with `407` are never counted), and released exactly once in
`HttpFilters.exchangeEnded`: after the response is written, or when the exchange was abandoned
(client gone, server failure, a filter's answer or abort, the proxy stopping). Put the limiter
first among the filters so the others do no work for refused requests.
- **Tunnels:** a `CONNECT` tunnel and an upgraded (WebSocket) connection hold their permit until
they close; `countTunnels(false)` stops counting `CONNECT`s and releases an upgrade's permit
after its `101`. An intercepted `CONNECT` holds its permit only until interception starts, and
the requests inside the session are counted one by one.
- **Safety net:** a permit held for longer than `permitTimeout` (10 minutes by default) is
reclaimed and logged at `WARNING`; established tunnels are exempt.
- **Shadow mode:** `shadow(true)` counts and reports requests over the limit but lets them
through, to try a limit out first.
- **Answers:** `retryAfter(Duration)` (or `null` for none) and `response((key, request, flow) ->
...)` replace the default `429`.
- **Metrics:** `snapshot()` returns the permits in use, waiting and refused per busy key, plus
totals (granted, refused, reclaimed). A key is forgotten as soon as none of its permits are in
use, so memory stays bounded by the exchanges in progress.
Waiting blocks only the client connection's virtual thread. The limiter reads request heads only,
so bodies keep the fast path. Lambda-built filters keep no state per exchange, so combine them
with a limiter in a chain rather than inside the builder. On the command line,
`--max-concurrent-per-client N` (or `max_concurrent_per_client=N`) installs one keyed by client IP.
# Chained proxies
How MicroProxy forwards traffic through other proxies: upstream HTTP, HTTPS and SOCKS proxies
chosen from URLs, `NO_PROXY` rules or the environment, failover and backoff between them, and what
happens when a client's own proxy sits behind an intercepting MicroProxy. Use this page to put
MicroProxy in front of (or behind) a corporate proxy.
Chained proxies are HTTP (with Basic credentials, optionally over TLS, spoken to in HTTP/1.1 or
[HTTP/2](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#http2-to-chained-proxies)), SOCKS4a and SOCKS5 (with username/password). A `ChainedProxyManager` chooses them per request, as a list of candidates: the
proxy falls back to the next proxy or a direct connection when one fails, optionally with
exponential backoff between attempts. `UpstreamProxyManager` configures them from proxy URLs,
`NO_PROXY` rules or the environment; a [Starlark script's `upstream` hook](https://mahmoudimus.github.io/MicroProxy/reference/starlark-api/)
can choose them too.
## Upstream proxies and NO_PROXY
`UpstreamProxyManager` is a `ChainedProxyManager` configured the way command-line clients are:
- **Upstreams:** one proxy URL for plain HTTP and optionally another for HTTPS/CONNECT:
`http://user:pw@proxy:3128`, `https://...` (TLS to the proxy), `h2://...` and `h2c://...`
([HTTP/2 to the proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#http2-to-chained-proxies)), `socks4://`, `socks5://`.
- **Bypass:** `NoProxyRules` takes curl's `NO_PROXY` syntax: `*`, domain suffixes
(`example.com`, `.example.com`, `*.example.com`), IP literals, CIDR ranges and `:port`
qualifiers. Addresses are only compared when the request names an IP literal, so no DNS
lookups happen.
- **Environment:** `UpstreamProxyManager.fromEnvironment(System.getenv())`, `--env-proxy` or
`use_env_proxy=true` read `http_proxy`, `https_proxy`, `all_proxy` and `no_proxy` like curl
does. Upper-case `HTTP_PROXY` is ignored, as curl ignores it, because CGI servers set it from a
request header.
```bash
java -jar microproxy.jar --upstream-proxy http://proxy.corp:3128 --no-proxy "localhost,.corp,10.0.0.0/8"
```
For servers or proxies signed by a private CA, `SslContexts.systemDefaultPlus(caCert)` trusts the
JDK's roots plus extra anchors and keeps host-name checks. Use it as the MITM manager's upstream
context, or as a chained proxy's.
### Retries and backoff
When a connection through one chained proxy fails (refused, timed out, its name does not resolve,
its TLS handshake or `CONNECT` fails, or, for an intercepted `CONNECT`, the TLS handshake with the
server through it fails, [before or after](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-tls-failures) the `CONNECT` is
answered), the proxy tries the next candidate the
`ChainedProxyManager` offered, which may be a direct connection. By default it moves on at once,
as LittleProxy does. `withChainedProxyRetryBackoff(RetryBackoff)` waits in between, so a flapping
upstream is not hammered. `RetryBackoff` has these policies:
| Policy | Waits before attempts 2, 3, 4, ... |
|---|---|
| `RetryBackoff.none()` | nothing (the default) |
| `RetryBackoff.exponentialWithJitter(initial, max)` | a uniformly random time between zero and `initial`, `2 × initial`, `4 × initial`, ... capped at `max` ("full jitter", so clients that failed together do not retry together) |
| `RetryBackoff.exponential(initial, max)` | exactly `initial`, `2 × initial`, `4 × initial`, ... capped at `max`, without jitter: deterministic doubling, as LittleProxy forks that added a backoff used, for tests that depend on exact timings |
| `RetryBackoff.fixed(delay)` | exactly `delay` every time |
```java
MicroProxy.bootstrap()
.withChainProxyManager(upstreams)
.withChainedProxyRetryBackoff(RetryBackoff.exponentialWithJitter(Duration.ofMillis(100), Duration.ofSeconds(2)))
.start();
```
`withChainedProxyRetryBackoff(initial, max)` is short for the jittered policy. In a properties file,
`chained_proxy_backoff_initial_ms` and `chained_proxy_backoff_max_ms` choose it, and
`chained_proxy_backoff_jitter=false` the deterministic one; on the command line,
`--chained-proxy-backoff [:]` and `--chained-proxy-backoff-deterministic
[:]`:
```bash
java -jar microproxy.jar --upstream-proxy http://proxy.corp:3128 --chained-proxy-backoff 100:2000
java -jar microproxy.jar --upstream-proxy http://proxy.corp:3128 --chained-proxy-backoff-deterministic 100:800
java -jar microproxy.jar --config proxy.properties --chained-proxy-backoff 0 # turn off the file's backoff
```
Without a maximum, the properties and the flags cap the wait at 8 × the initial one; `0` on the
command line turns waiting off, overriding a properties file. A request's filters can choose
another policy for it, including `none()`, with `HttpFilters.chainedProxyRetryBackoff()` (see
[Filters](https://mahmoudimus.github.io/MicroProxy/guides/filters/#per-request-retry-backoff)).
- It never waits before the first attempt or after the last one.
- It stops waiting, and gives up on the request, if the client disconnects meanwhile. Once the
client has sent more bytes (a request body, a pipelined request) a disconnect can no longer be
seen, and the rest of the wait is a plain sleep.
- By default the waits of one request add up to at most 30 seconds or the connect timeout,
whichever is less, so a long candidate list cannot hold a request for minutes. Once that budget
is spent, the remaining candidates are tried without waiting (the wait that would overrun it is
shortened to what is left). The budget is part of the policy: `withBudget(Duration)` sets
another, and `unbudgeted()` lifts it, so that every wait the policy names is waited, as
LittleProxy forks that added a backoff did. In a properties file,
`chained_proxy_backoff_budget_ms` takes a number of milliseconds or `unlimited`:
```java
// 100, 200, 400, 800, 1600 ms between six candidates, whatever the connect timeout
RetryBackoff.exponential(Duration.ofMillis(100), Duration.ofSeconds(5)).unbudgeted()
```
- The waiting counts towards the connect phase of `FlowContext.timings()`; it fires no callbacks,
only a `DEBUG` log line.
## HTTP/2 to chained proxies
A chained HTTP proxy is spoken to in HTTP/1.1 by default: a connection of its own for each
tunnel, and plain-HTTP requests on (pooled) connections one at a time. A chained proxy that speaks
HTTP/2 can carry all of them as streams of a few shared connections instead (RFC 9113 section
8.5), as forward proxies such as MicroProxy itself (on its TLS listener, where HTTP/2 is on by default,
or with `--http2-cleartext`) do. It is chosen per proxy: the `h2://` and `h2c://` schemes in a proxy URL,
or `ChainedProxy.http2()`, which is `false` by default.
| Proxy URL | Means | Bootstrap |
|---|---|---|
| `h2://[user:pw@]host[:port]` | TLS to the proxy (port 443 by default), offering `h2` and `http/1.1` with ALPN | `http2()` and `requiresEncryption()` return `true` |
| `h2c://[user:pw@]host[:port]` | HTTP/2 with prior knowledge over plain TCP (port 80 by default) | `http2()` returns `true` |
Proxy URLs are taken by `--upstream-proxy` and `--upstream-https-proxy`, the `upstream_proxy` and
`upstream_https_proxy` properties, `http_proxy`, `https_proxy` and `all_proxy` (`--env-proxy`),
`UpstreamProxyManager.proxy(url)` and a script's `upstream()`.
```bash
java -jar microproxy.jar --upstream-proxy h2://alice:s3cret@proxy.corp:443 --mitm
java -jar microproxy.jar --upstream-proxy h2c://127.0.0.1:3128
```
- **What takes a stream.** Every `CONNECT` the proxy sends the chained proxy is a stream: a
client's `CONNECT` it relays, the tunnel of an intercepted one (its TLS with the server, and
HTTP/2 with the server if both choose it, then run inside the stream), the tunnel it opens to
[originate TLS](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-origination-https-uris-without-connect) for an `https://`
request, and the tunnels to [a client's own proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy) reached
through it. A
plain-HTTP request is a stream too, as a proxy request: `:scheme http`, `:authority` the
server's, and `:path`. A WebSocket upgrade over plain HTTP still goes over HTTP/1.1.
- **Shared connections.** A connection to the chained proxy is keyed by the proxy alone (its
address, TLS, `SSLContext` instance, credentials and local address), never by the server, and
carries the streams of every client connection; it is never coalesced. The proxy reads the
chained proxy's SETTINGS first and opens no more streams than its
`SETTINGS_MAX_CONCURRENT_STREAMS`; when every connection is full it opens another. A burst of
exchanges waits for the connection one of them is making. Connections close after the idle
timeout without streams.
- **Negotiation.** Over TLS, `h2` and `http/1.1` are offered with ALPN (before the proxy's
`configure`, which may change that); a chained proxy that chooses `http/1.1`, or no protocol,
is spoken to in HTTP/1.1, as without HTTP/2. An `h2c://` proxy must speak HTTP/2 with prior
knowledge: one that does not fails as an unreachable proxy does.
- **Credentials.** The proxy's credentials go on every stream, as `proxy-authorization` (Basic).
As over HTTP/1.1, a `407` to a plain-HTTP request is relayed to the client (without its
challenge, which is not the client's to answer), and a `CONNECT` the chained proxy refuses is a
`502`.
- **Tunnels.** A tunnel's bytes are DATA frames, flow controlled both ways: a client that stops
reading stops the chained proxy's stream, never the connection. One side's end is END_STREAM
(and the chained proxy's END_STREAM ends the client's side), and a reset of the stream fails
the tunnel as a reset socket does. The PROXY protocol header (`--send-proxy-protocol`) goes into
the tunnel, as through an HTTP/1.1 chained proxy.
- **Failover.** Failing to reach the chained proxy (refused, timed out, TLS failed, its preface
not HTTP/2) or a refused `CONNECT` moves on to the route's next proxy (or a direct connection),
with the [backoff](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#retries-and-backoff) and the `failover` and `backoff_*` stats, as over
HTTP/1.1. A GOAWAY or a connection failure affects only the streams of that connection; others
go on. A stream the chained proxy never processed (above GOAWAY's last stream id, or answered
`REFUSED_STREAM`) is sent again on another connection, and after two such streams an exchange
moves on to the next proxy. A stream that started (a tunnel that opened, a request whose
response was under way) is not sent again: it fails as with any server connection, with the
response flags that say so (`UC` when the connection went away).
- **Stats and logs.** The chained proxy's cluster (`cluster..*`) counts its HTTP/2
connections in `upstream_cx_total` and `upstream_cx_http2_total` (not each tunnel), the
requests on them in `upstream_rq_*`, and their bytes, frames included, in
`upstream_cx_rx_bytes_total` and `_tx_bytes_total`; `http2.upstream.*` counts their streams
with those to servers. In the access log, `%UPSTREAM_CLUSTER%` names the chained proxy and
`%MP_UPSTREAM_PROTOCOL%` says `HTTP/2` (`FlowRecord.upstreamProtocol()`, and
`microproxy.upstream_protocol` in [OpenTelemetry](https://mahmoudimus.github.io/MicroProxy/observability/opentelemetry/)).
- **Needs** the `http2-codec` jar (bundled in the `-all` jar): an `h2://` or `h2c://` URL without
it fails at startup; a `ChainedProxy` whose `http2()` says `true` is spoken to in HTTP/1.1.
## In front of an HTTP or HTTPS proxy
A client may be set up to use an HTTPS proxy, such as a corporate egress proxy `corp.example:443`
that speaks TLS (and often HTTP/2) on its listener, with MicroProxy in front of it intercepting
the client's `CONNECT corp.example:443`. Inside that session the client asks what it takes for its
proxy for a tunnel per site: a `CONNECT` stream over HTTP/2 (`:authority github.com:443`, no
`:protocol`), or `CONNECT github.com:443 HTTP/1.1` over HTTP/1.1, and plain-HTTP URIs as proxy
requests: `GET http://example.com/ HTTP/1.1` (absolute-form), or a stream with `:scheme http` and
`:authority example.com`.
A client may use a plain-HTTP forward proxy the same way, such as a Squid at `corp.example:3128`:
it sends `CONNECT corp.example:3128`, then speaks plain HTTP in the tunnel, which MicroProxy (with
a MITM manager) serves as a plain session ([Deciding what to
intercept](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)). Inside it the client sends what it would
send that proxy: absolute-form requests, and `CONNECT github.com:443 HTTP/1.1`.
In both cases MicroProxy forwards these proxy requests to the session's server, as the client
intended, rather than to `github.com` or `example.com` itself, which would bypass the proxy the
client chose. (Without a MITM manager, the `CONNECT` to the proxy is a tunnel, and everything in it
reaches the proxy untouched.)
```java
MicroProxy.bootstrap()
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca))
// HTTP/2 to servers (on by default) puts CONNECT streams on the HTTP/2 connection to the proxy
.withFiltersSource((request, ctx) -> new HttpFiltersAdapter(request, ctx) {
@Override
public boolean proxyToServerAllowMitm() {
// Intercept the session with the proxy, but not the tunnels through it.
return ctx.forwardedVia().isEmpty();
}
})
.start();
```
- **Upstream.** The session's server is a chained proxy for the tunnel. When MicroProxy's
connection to an HTTPS proxy negotiated HTTP/2 (`withHttp2Upstream`, on by default), the `CONNECT` is a stream
on that connection, multiplexed with the session's requests. Otherwise it goes on an HTTP/1.1
connection to it, over TLS to an HTTPS proxy and plain TCP to an HTTP one: an idle one of the
session's, or one made as the session's are (the same routes, the MITM manager's trust and
`configureServerSocket`, the same isolation; through an HTTP chained proxy, a tunnel to it).
Connections that carry a client's credentials for the proxy are never pooled or shared with
other clients.
- **Proxy requests.** An absolute-form request goes to the proxy as it was sent: absolute-form,
with `Host` from its URI; over HTTP/2 to an HTTPS proxy, as `:scheme http` and the target's
`:authority`. It goes on the session's own connections to the proxy, never pooled ones (nor ones
that hand an HTTP chained proxy its requests), by the routes to the proxy.
- **Fields, both ways.** The client's fields go with the `CONNECT` or the proxy request,
`Proxy-Authorization` included (those credentials are for the proxy; MicroProxy's own
authentication covered the outer `CONNECT`), without hop-by-hop fields and with `Via`. The
proxy's answer reaches the client as it was sent: a `407` with its `Proxy-Authenticate`
challenge, a `403`, the response it fetched, or the `2xx` that opens the tunnel. An HTTP/1.1
client can retry on the same session after a refusal.
- **Filters.** A proxy request is a request of the session: `clientToProxyRequest`,
`proxyToServerRequest` (which may rewrite a `CONNECT`'s target), then `serverToProxyResponse`
and `proxyToClientResponse` for the proxy's answer; a short-circuit response (a `403`, say)
rejects it. `FlowContext.forwardedVia()` names the proxy (`host:port`) for it and for the
requests inside the tunnel a `CONNECT` opens; scripts see `ctx.forwarded_via`.
- **The client's URIs, and the filters'.** Only an absolute URI the client sent itself makes a
proxy request. A filter or script that gives a request an absolute URI of its own (`MapRemote`,
`req.uri = "https://..."`) sends it straight to that URI's server, as in any intercepted
session, and `forwardedVia()` is then empty; one that leaves the client's URI as it was keeps it
a proxy request. A client's absolute-form request to a server that is not a proxy is legal, if
rare, and goes there as sent too: the client chose that server.
- **The tunnel.** Once the proxy has opened it, the tunnel goes on as a `CONNECT` to MicroProxy
would ([Deciding what to intercept](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)), with the
proxy's tunnel as the way to the target. Without interception for the target
(`proxyToServerAllowMitm`, `allow_mitm` in scripts, an ignore rule for the target, or a MITM
manager that declines), its bytes are relayed as they are. Otherwise the client's first bytes
decide: a ClientHello goes through the host rules (with its SNI), `shouldIntercept` and
`proxyToServerAllowMitm(clientHello)`, and when intercepted, MicroProxy's own TLS to the target
runs inside the tunnel, offering the client's ALPN protocols, and the client gets the target's
choice; plain HTTP is served as HTTP, its requests going through the filters; anything else, a
declined ClientHello, a [TCP host](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/#raw-tcp-streams) or a server that speaks first (noticed
through the tunnel) is relayed as it is, through the TCP hooks, as is intercepted TLS that does
not carry HTTP. Relayed bytes go under flow control both ways; END_STREAM and half-closes go
through (over an HTTP/1.1 TLS connection to the proxy, where TLS 1.3 allows one). Every other
connection to the target, for the requests of an intercepted or plain session in the tunnel, is
a tunnel through the same proxy, opened with the same `CONNECT` fields, never pooled;
`FullFlowContext.getChainedProxy()` names the proxy. A target that does not speak TLS to a
client that does gets a plain tunnel, a new one.
- **Resets.** A client's reset of the stream resets the stream to the proxy (or closes the
connection to it), and once the tunnel is open it is not a rapid reset. A reset from the proxy,
or its connection closing, resets the client's stream.
Over HTTP/2, `:scheme https` requests for another authority still get `421` ([connection
coalescing](https://mahmoudimus.github.io/MicroProxy/guides/http2/#connection-coalescing)), and so does a WebSocket's extended `CONNECT`, which is not forwarded.
# Transparent mode
Proxy traffic that a firewall or gateway redirects to MicroProxy instead of sending it there: TLS
routed by its SNI, plain HTTP by its `Host`, and QUIC on a UDP port of its own, intercepted or
relayed. Use this page to set up a transparent proxy with iptables; the clients then need no proxy
settings, only the proxy's CA for intercepted hosts.
## Transparent TLS and HTTP
`--transparent` (`withTransparent(true)`, `transparent=true` or `mode=transparent`) is for
traffic redirected to the proxy rather than sent to it, by a firewall rule or a gateway. Besides
leaving out `Via` and keeping hop-by-hop headers, the listener then accepts:
- **TLS without `CONNECT`.** A connection that starts with a TLS record is routed by its
ClientHello's SNI, to that host's port 443 (`--transparent-tls-port`, `transparent_tls_port`),
and treated like a `CONNECT` to it: filters see a `CONNECT` the proxy made up (it gets no answer
on the wire), and the host rules, hooks and interception apply. Without a MITM manager, such
connections are tunnelled: an SNI proxy.
- **Plain HTTP**, routed by its `Host` field, when origin-form requests are allowed
(`--allow-requests-to-origin-server`, `allow_requests_to_origin_server=true`), as before.
- **QUIC** on a UDP port of its own, with `--transparent-quic-port `
(`withTransparentQuicPort`, `transparent_quic_port`): routed by the SNI of the ClientHello in
its first packets, then intercepted (HTTP/3, or other protocols stream by stream) or relayed by
the same rules (see [Transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic)).
Java cannot read a redirected connection's original destination (Linux's `SO_ORIGINAL_DST`), so
the server is the one the SNI or the `Host` field names, not the address the client connected to.
The client choosing the server is by design here, as it is for any forward proxy: a transparent
proxy takes the client where it was going. Put it where only the clients it serves can reach it.
Inside an SNI-routed session, an absolute URI is a request for that session's server (as for a
proxy, [In front of an HTTP or HTTPS proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy)), never for the URI's authority.
A ClientHello without SNI (clients connecting to an IP address send none) cannot be routed: the
proxy answers with a TLS `unrecognized_name` alert, closes the connection and reports it to
`ActivityTracker.connectionExceptionCaught`. Such clients cannot send proxy credentials: with
proxy authentication, their connections are closed unless the authenticator accepts them without.
Redirecting a machine's outgoing web traffic to a proxy on port 8080, running as the user `proxy`
so that its own connections are not redirected again:
```bash
iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner proxy --dport 80 -j REDIRECT --to-ports 8080
iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner proxy --dport 443 -j REDIRECT --to-ports 8080
# On a gateway, for the clients behind it (eth1), use PREROUTING instead:
# iptables -t nat -A PREROUTING -i eth1 -p tcp -m multiport --dports 80,443 -j REDIRECT --to-ports 8080
sudo -u proxy java -jar microproxy.jar --server --port 8080 --transparent --mitm \
--allow-requests-to-origin-server --ignore-hosts '\.bank\.example:443$'
```
Clients must trust the proxy's CA (`microproxy-ca.pem`) for the hosts that are intercepted. QUIC
(UDP 443) is not redirected by these rules: redirect it to a [transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic)
port too, or block it so that browsers fall back to TCP.
## Transparent QUIC
`--transparent-quic-port ` (`withTransparentQuicPort(port)`, `transparent_quic_port`, with
`--transparent`) opens a UDP listener for QUIC that a firewall redirects to the proxy: the UDP
counterpart of [transparent TLS](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-mode). Java cannot read a redirected datagram's
original destination either, so each new client flow (a client address and the Destination
Connection ID of its first Initial packets) is routed by its SNI, to that host's
`--transparent-tls-port` (443).
- **Reading the ClientHello.** The proxy holds a new client's datagrams and decrypts its Initial
packets with the keys their Destination Connection ID gives (RFC 9001 section 5.2, QUIC v1 and
v2), without a handshake, as mitmproxy does. It puts the CRYPTO frames back together across
packets and datagrams, in any order (browsers' post-quantum ClientHellos take two packets, their
frames shuffled), and parses the same `ClientHello` that TLS over TCP gives.
- **Deciding**, as for transparent TLS: a `CONNECT sni:443` is made up for the flow (filters see
it; `clientToProxyRequest` answering it drops the flow; `proxyToServerAllowMitm()` and
`allow_mitm` may decline), then the [ignore and allow rules](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#ignoring-and-allowing-hosts),
the client's ALPN (`h3` is served as HTTP/3; other protocols are intercepted
[stream by stream](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/) with a `QuicInterceptor` or for the `--quic-hosts`, and
relayed otherwise), `MitmManager.shouldIntercept(hello, flow)`, and
`proxyToServerAllowMitm(hello)` / `on_client_hello` (the table below). A redirected client
sends no proxy credentials: with an authenticator, it must accept clients without.
- **Intercepted** (HTTP/3 with a MITM manager, which must be a `CertificateAuthorityMitmManager`
to issue QUIC's certificates): the held datagrams go to the proxy's QUIC server, whose handshake
presents a certificate the CA issues for the SNI, and the connection is served HTTP/3 like a
[reverse proxy's](https://mahmoudimus.github.io/MicroProxy/guides/http3/#serving-http3-in-reverse-mode) clients, with its requests going to the SNI's
host: over HTTP/3 with `--http3-upstream` (the proxy takes the client's HTTP/3 as the origin's
advertisement, and falls back to TCP if its QUIC handshake fails), else over HTTP/2 or
HTTP/1.1. Filters, scripts (`on_request`, `on_frame`, ...), logging and trackers see the
requests, with `FlowContext.isTransparentQuic()` (and `ctx.transparent_quic`) true.
- **Intercepted stream by stream** (another ALPN such as DNS over QUIC's `doq` with a
`QuicInterceptor`, or a `--quic-hosts` host): the proxy connects to the server first, then
completes the client's handshake with the server's choice of ALPN, and relays the two
connections' streams and datagrams through the [QUIC stream hooks](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/).
- **Relayed** (no MITM manager, an ignored or declined host, another ALPN without a
`QuicInterceptor`): the datagrams go to the server untouched from a UDP socket of the flow's
own, and the server's back to the client from the listener's socket, until the flow is idle for
`--transparent-quic-relay-idle-timeout` (60 s). The client completes its handshake with the
real server.
- **Demultiplexing.** One socket serves every client. Intercepted connections are routed by the
connection IDs the proxy issued (so they survive the client moving to new ones); relayed flows
by the client address and the connection IDs seen in their long headers, and otherwise to the
client address's most recently active flow (servers issue more IDs in encrypted frames).
- **Clients that move** (QUIC connection migration, RFC 9000 section 9: a NAT that rebinds the
client to a new port, a phone that goes from Wi-Fi to cellular, a client that migrates on
purpose). An intercepted connection, HTTP/3 or [stream by stream](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/), follows
its client as any QUIC server does: its datagrams are routed by connection ID from any address,
the proxy validates the new path with PATH_CHALLENGE, sends there no more than three times what
it received until the client answers, keeps the congestion state for a new port and starts it
over for a new address, goes back to the old path if the new one does not answer, and answers
from the new path. The proxy's own connection to the server (stream by stream, and HTTP/3 to
origins) stays where it is. A relayed flow is harder: the proxy cannot read the client's packets,
nor the encrypted frames in which the server gives the client the connection IDs it moves to.
A short header packet from an address with no flow goes to the relayed flow that knows its
connection ID (from any address), else to the flow of the same client IP address whose known
server IDs share the longest prefix of at least 4 bytes with it (servers that encode routing in
their IDs), else to that IP address's most recently active relayed flow, which is what a NAT
rebinding needs. The flow then sends the server's datagrams to the new address, as a NAT sends
replies to its latest mapping, and learns the client's new ID when the server's IDs all have
one length. Limits: a relayed client that changes both IP address and connection ID is found
only by an ID prefix; with several relayed flows from one IP address the fallback may pick the
wrong one; and since the proxy can neither validate the new address nor tell a newer packet from
a replayed one, a sender that can spoof the client's IP address with one of the flow's
connection IDs can turn its replies elsewhere until the client sends again (the end-to-end
connection stays the client's: the server, whose path to the proxy does not change, validates
nothing). A server that sends `disable_active_migration` is not migrated away from by clients
that honour it.
- **Versions.** A new client offering a version the proxy cannot read gets Version Negotiation
(v1, v2), since its destination is in a ClientHello the proxy cannot decrypt; on an existing
relayed flow, other versions are relayed like anything else.
- **Retry.** `--http3-retry` (`Http3Options.retry`) makes intercepted clients prove their address
first. Relayed clients never get one: their server would refuse the proxy's token.
The decision, in order; the first row that applies decides:
| The flow | Becomes |
|---|---|
| A `clientToProxyRequest` answers the made-up `CONNECT`, or the authenticator refuses a client without credentials | dropped |
| No MITM manager, or `proxyToServerAllowMitm()` / `allow_mitm` declines | relayed |
| An [ignore or allow rule](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#ignoring-and-allowing-hosts) leaves out the server or the SNI | relayed |
| ALPN without `h3`, not a `--quic-hosts` host, and no `QuicInterceptor` (`on_quic_*`) | relayed |
| No SNI (only inside a CONNECT-UDP flow to an IP address), or no `CertificateAuthorityMitmManager` to issue the certificate | relayed |
| `MitmManager.shouldIntercept(hello, flow)` or `proxyToServerAllowMitm(hello)` / `on_client_hello` declines | relayed |
| A `--quic-hosts` host (whatever its ALPN, `h3` included) | intercepted [stream by stream](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/) |
| ALPN offering `h3` | intercepted as HTTP/3 |
| Any other ALPN (`doq`, `hq-interop`, a protocol of its own), with a `QuicInterceptor` | intercepted [stream by stream](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/) |
`ActivityTracker.transparentQuicFlowDecided` reports each decision (`intercepted` is true for
both kinds of interception, the reason saying why for stream by stream).
- **Bounds** (`Http3Options`): new clients held at once (1024, and 32 per client IP address, each
holding 16 datagrams and 64 KiB at most, all together 8 MiB), how long one may take to send its
ClientHello and be decided on (5 s), relayed flows (4096, and 256 per client IP address).
Datagrams past a bound are dropped, as a full socket buffer would drop them.
```java
MicroProxy.bootstrap()
.withTransparent(true)
.withTransparentQuicPort(8443) // --transparent-quic-port 8443
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca))
.withHttp3Upstream(true) // HTTP/3 to origins too
.withIgnoreHosts("\\.bank\\.example:443$") // relayed untouched
.start();
```
Redirecting a gateway's clients' QUIC (`eth1`) to UDP port 8443, next to their TCP:
```bash
# REDIRECT (NAT): the proxy answers from its listener socket, and the kernel's connection
# tracking turns the replies' source back into the address the client sent to.
iptables -t nat -A PREROUTING -i eth1 -p udp --dport 443 -j REDIRECT --to-ports 8443
# On the proxy's own machine, its own QUIC (relays, HTTP/3 to origins) must not be redirected:
iptables -t nat -A OUTPUT -p udp -m owner ! --uid-owner proxy --dport 443 -j REDIRECT --to-ports 8443
# TPROXY keeps the original destination on the packet instead of rewriting it:
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
iptables -t mangle -A PREROUTING -i eth1 -p udp --dport 443 -j TPROXY --on-port 8443 --tproxy-mark 1
```
### NAT
Use REDIRECT. With it, replies must leave from the socket the client's datagrams arrived
at, as the proxy's do, so that connection tracking maps them back; and the conntrack entry of a
UDP flow (`nf_conntrack_udp_timeout_stream`, 120 s by default) should outlive the relay's idle
timeout. TPROXY needs a listening socket with `IP_TRANSPARENT`, which the JDK cannot set, and
replies sent from the original destination address, which the JDK cannot do either: it does not
work with MicroProxy as it is. Relayed flows reach servers from a new UDP socket each, so a
firewall in front of the proxy sees each as a flow of its own. A client the NAT maps to a new port
is followed (see **Clients that move** above).
# Reverse proxy
Serve one upstream server, over HTTP, HTTPS, HTTP/2, HTTP/3 or raw TCP, so that clients talk to
MicroProxy as if it were that server. Use this page to put MicroProxy in front of a backend; HTTP/3
to clients is described in [Serving HTTP/3 in reverse
mode](https://mahmoudimus.github.io/MicroProxy/guides/http3/#serving-http3-in-reverse-mode).
## Reverse proxy mode
`--mode reverse:https://example.com` (or `--reverse https://example.com`,
`withReverseProxy("https://example.com")`, `mode=reverse:https://example.com`) makes the proxy a
server for one upstream, in mitmproxy's specification syntax: `[scheme://]host[:port]`, the
scheme `https` by default, the port 443 for `https` and 80 for `http`.
- Every request goes to the upstream, whatever it names. Origin-form requests (`GET /path`) are
accepted, and absolute-form ones are made origin-form. As in an intercepted session, a filter
that gives a request an absolute URI of its own (`MapRemote`, a script assigning `req.uri`)
sends it to that URI's server instead.
- **The client never picks the server.** Nothing a client sends chooses where its request goes,
so a reverse proxy cannot be used to reach other hosts (server-side request forgery): an
absolute-form URI (`GET http://127.0.0.1:6379/`) is made origin-form for the upstream before
any filter sees it, as mitmproxy's reverse mode sets the upstream's address on every request,
and its authority survives only as the `Host` that `--keep-host-header` keeps; an HTTP/2
`:authority` chooses nothing either; and `CONNECT` is refused with `400`, with or without a
MITM manager, as mitmproxy refuses it outside its regular and upstream modes. Only filters and
scripts may send a request elsewhere: an absolute URI that a filter sets is followed, the one
the client sent never is.
- `Host` is set to the upstream's (`example.com`, with the port unless it is the scheme's
default) before any filter sees the request, unless `--keep-host-header` (`keep_host_header`,
`withKeepHostHeader(true)`) keeps the client's.
- `https://` upstreams are reached over TLS, validated with the MITM manager's server context
when one is configured (`--mitm-trust-all` works too) and the JVM's trust store otherwise, and
over HTTP/2 when the upstream offers it (unless `--no-http2-upstream`). `http://` upstreams get plain HTTP.
An upstream named by its IP address (`reverse:https://10.0.0.7`) gets no SNI and its
certificate is checked against that address; a filter's `proxyToServerTlsIdentity` can send the
service's name and check the certificate against it instead
([Server name and verification host](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-name-and-verification-host)).
- `tcp://host:port` relays each client connection to the upstream as raw bytes.
- With a TLS listener (`withSslContextSource`, `--listener-keystore`), the proxy terminates TLS,
and serves HTTP/2 to clients that offer it (unless `--no-http2`). With `--http3`, it serves HTTP/3 on
the UDP port with the same number, and advertises it with `Alt-Svc` (see
[HTTP/3 and QUIC](https://mahmoudimus.github.io/MicroProxy/guides/http3/)); with `--http3-upstream`, it speaks HTTP/3 to an upstream
that offers it.
- Filters, scripts, the cache, logging and failure answers work as for any request.
# SOCKS
Serve SOCKS5, SOCKS4 and SOCKS4a clients, on a listener of their own or next to HTTP clients on the
regular one; their connections go through the same filters, routing and interception as a
`CONNECT`. Use this page for clients that speak SOCKS (curl, browsers, SSH); chaining to an
upstream SOCKS proxy is in [Chained proxies](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/).
## SOCKS5 mode
`--mode socks5` (`withSocksServer(true)`, `mode=socks5`) makes the listener a SOCKS server, as
mitmproxy's `socks5` mode does: SOCKS5 ([RFC 1928](https://www.rfc-editor.org/rfc/rfc1928)), and
SOCKS4 and 4a as well. `--socks` (`withSocksDetection(true)`, `socks=true`) serves SOCKS clients on
the regular listener besides HTTP ones, told apart by their first byte: `0x05` for SOCKS5, `0x04`
for SOCKS4 and 4a, which no HTTP request, TLS record or HTTP/2 preface starts with.
```bash
java -jar microproxy.jar --port 1080 --mode socks5 --mitm
curl --socks5-hostname 127.0.0.1:1080 http://example.com/ # the proxy resolves the name
curl --proxy socks5h://127.0.0.1:1080 --cacert microproxy-ca.pem https://example.com/ # intercepted
curl --socks5 127.0.0.1:1080 http://example.com/ # curl resolves, sends an address
curl --socks4a 127.0.0.1:1080 http://example.com/
java -jar microproxy.jar --port 8080 --socks --mitm # HTTP and SOCKS clients on one port
curl --proxy socks5h://127.0.0.1:8080 https://example.com/ --cacert microproxy-ca.pem
curl --proxy http://127.0.0.1:8080 https://example.com/ --cacert microproxy-ca.pem
```
With proxy authentication, SOCKS5 clients give a username and password:
```java
MicroProxy.bootstrap()
.withPort(1080)
.withSocksServer(true)
.withProxyAuthenticator((user, password) -> "alice".equals(user) && "secret".equals(password))
.start();
```
```bash
curl --proxy socks5h://alice:secret@127.0.0.1:1080 http://example.com/
```
| Flag | Property | Bootstrap | |
|---|---|---|---|
| `--mode socks5` | `mode=socks5` | `withSocksServer(true)` | the listener serves SOCKS clients only; others are closed |
| `--socks` | `socks=true` | `withSocksDetection(true)` | the regular listener serves SOCKS clients too |
| `--tls-handshake-timeout` | `tls_handshake_timeout` | `withTlsHandshakeTimeout` | also bounds the SOCKS handshake, up to the request |
### One request, one `CONNECT`
Only the `CONNECT` command is served; `BIND` and `UDP ASSOCIATE`
are answered "command not supported". The destination (an IPv4 or IPv6 address, or a host name,
which the proxy resolves) becomes a `CONNECT` the proxy makes up, which then goes exactly where an
HTTP client's `CONNECT` goes:
- the filters see it (`clientToProxyRequest`, `proxyToServerRequest`, `proxyToServerAllowMitm`,
...), and so do `ActivityTracker`s, the access and request logs, `FlowContext.timings()` and
scripts; its answer is the `200` the client would have got, or the failure;
- routing: the `ChainedProxyManager`, chained HTTP and SOCKS proxies, `NO_PROXY` rules, backoff;
- then the client's first bytes decide, as after a `CONNECT`
([Deciding what to intercept](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)): a ClientHello is
intercepted with the MITM manager unless the [host rules](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#ignoring-and-allowing-hosts), its SNI
and ALPN, the manager or a filter decline it; plain HTTP is served as HTTP, its requests going to
the destination through the filters; anything else is tunnelled;
- the [TCP hooks](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/#raw-tcp-streams) see its tunnels as they see a `CONNECT`'s, and an intercepted
TLS session that does not carry HTTP (or one with a [TCP host](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/#raw-tcp-streams)) is relayed
decrypted through them.
A SOCKS client has no other way to send plain HTTP through the proxy, so its first bytes are read
even without `--mitm`: its HTTP requests are filtered and logged, and its TLS is tunnelled.
`FlowContext.socksVersion()` (5, or 4 for SOCKS4 and 4a; empty for HTTP clients) and Starlark's
`ctx.socks_version` tell SOCKS clients apart.
### Connected, then answered
As mitmproxy does, the proxy connects to the server (or through its
chained proxy) before it answers the request, so the client learns why a connection failed. The
ClientHello is read only after the answer, since a client sends it only then; this is the order of
an HTTP `CONNECT`, whose `200` is written once the server is connected and whose server handshake
waits for the ClientHello (ALPN mirroring included).
| Failure | SOCKS5 reply |
|---|---|
| The host name does not resolve (`UnresolvedHost`) | `0x04` host unreachable |
| The connection is refused | `0x05` connection refused |
| No route to the host / the network | `0x04` host unreachable / `0x03` network unreachable |
| The connect timeout passed | `0x06` TTL expired |
| The authenticator (checking every request), a filter or the `ChainedProxyManager` refused it | `0x02` not allowed by ruleset |
| Anything else (a chained proxy failed, the pool is exhausted, a filter answered with a `5xx`) | `0x01` general failure |
SOCKS4 replies have only "granted" (90) and "rejected" (91).
### Authentication
Without a [proxy authenticator](https://mahmoudimus.github.io/MicroProxy/guides/authentication/), SOCKS5 clients must
offer "no authentication". With one, they authenticate with a username and password
([RFC 1929](https://www.rfc-editor.org/rfc/rfc1929)), which the authenticator sees as
`Proxy-Authorization: Basic` on a made-up `CONNECT` whose target is not known yet (`0.0.0.0:0`),
so `authenticate(user, password)`, request-aware authenticators and a script's `authenticate` hook
all work, and the accepted user is `ClientDetails.getUserName()`. A client that offers no
credentials, SOCKS4's included (its user id is not a credential), is let in only if the
authenticator accepts that request without them; otherwise SOCKS5 clients get "no acceptable
methods" (`0xFF`) and SOCKS4 clients a rejection. With `authenticateEveryRequest()`, the request's
own `CONNECT`, with its destination and the same credentials, is authenticated again.
### Hardening
The handshake, from the first byte to the request, must finish within the TLS
handshake timeout (`--tls-handshake-timeout`, 10 seconds by default; with `0`, each read within the
idle timeout). SOCKS5 fields are bounded by their one-byte lengths, SOCKS4's user id and host name
by 255 bytes; host names may hold only letters, digits, `.`, `-` and `_` (or an IP literal).
Not supported: `BIND`, `UDP ASSOCIATE` (and so DNS or QUIC through SOCKS), GSSAPI authentication,
SOCKS on a TLS listener or together with a reverse proxy, and mitmproxy's `@port` listen suffix
(use `--port`).
# HTTP/2
Speak HTTP/2 to clients (inside intercepted TLS, on the proxy's TLS listener, and with prior
knowledge on the plain listener) and to servers, with connection sharing and coalescing, trailers
and gRPC, CONNECT and WebSockets over HTTP/2, and hardened limits. Use this page to configure
HTTP/2 (on by default over TLS) and to understand how streams map to exchanges; frame-level hooks
are in [Frame interception](https://mahmoudimus.github.io/MicroProxy/guides/frame-interception/).
MicroProxy can speak HTTP/2 to clients inside intercepted TLS sessions and, with prior knowledge
(`h2c`), on its plain listener, and to origin servers that negotiate it. Each HTTP/2 stream is an
exchange like any HTTP/1 request: it gets its own `HttpFilters`, cache lookup, failure answers,
trackers and log lines. The client side and the server side are independent: an HTTP/1 client
can reach an HTTP/2 server and the other way round.
```java
MicroProxy.bootstrap()
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca))
// HTTP/2 to clients and to servers is on by default:
// .withHttp2(false) // to clients: --no-http2, http2=false
// .withHttp2Upstream(false) // to servers: --no-http2-upstream, http2_upstream=false
.withHttp2Cleartext(true) // h2c (opt-in): --http2-cleartext, http2_cleartext=true
.withHttp2Options(Http2Options.builder().maxConcurrentStreams(200).build()) // optional
.start();
```
```bash
java -jar microproxy-starlark-all.jar --port 8080 --mitm # h2 to clients and servers that offer it
java -jar microproxy-starlark-all.jar --port 8080 --mitm --no-http2 --no-http2-upstream # HTTP/1.1 only
java -jar microproxy-starlark-all.jar --port 8080 --http2-cleartext # h2c on the plain listener too
```
HTTP/2 to clients (`withHttp2`, `--http2`, `http2`) and to servers (`withHttp2Upstream`,
`--http2-upstream`, `http2_upstream`) is **on by default**: a client that offers `h2` on
intercepted TLS or the proxy's TLS listener gets it, and so does a server that the proxy offers
`h2` and that picks it. Peers that offer only HTTP/1.1, or no ALPN, keep HTTP/1.1, so the default
changes nothing for them. Turn either off with `false` (`--no-http2`, `--no-http2-upstream`,
`http2=false`, `http2_upstream=false`). HTTP/2 with prior knowledge on the plain listener (`h2c`,
`withHttp2Cleartext`) stays opt-in, as does an [h2c-only listener](https://mahmoudimus.github.io/MicroProxy/guides/http2/#h2c-only-listener).
HTTP/2 needs the optional `http2-codec` module on the class path (see
[Install](https://mahmoudimus.github.io/MicroProxy/getting-started/install/#modules-and-artifacts); the `microproxy-starlark` `-all`
jar bundles it). Without it:
- left at their defaults, HTTP/2 to clients and to servers falls back to HTTP/1.1, which the proxy
logs once at `INFO` (`org.microproxy.impl.Http2Support`), and nothing loads the module;
- enabled explicitly (`withHttp2(true)`, `withHttp2Upstream(true)`, `withHttp2Cleartext(true)`,
their flags or properties), the proxy fails at startup with an `IllegalStateException` that says
so, since it cannot do what it was asked.
## HTTP/2 to clients
### Negotiation
With HTTP/2 on (the default), both intercepted TLS and the proxy's own TLS listener
(`withSslContextSource`) offer `h2` and
`http/1.1` through ALPN. A client that offers `h2` gets it, unless an intercepted server that was
offered `h2` chose `http/1.1` (see [ALPN mirroring](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#alpn-mirroring)). Any other client keeps HTTP/1.1 as
before: one that offers only `http/1.1`, one that offers protocols the proxy does not know, and
one that sends no ALPN at all (the handshake never fails over ALPN). The TLS log line for the
handshake shows the result (`alpn=h2`). The TLS listener retains its configured certificate,
client authentication and socket settings. Its HTTP/2 streams are forward-proxy requests, like
`h2c`, and authenticate separately when proxy authentication is configured.
### What a stream looks like to filters
- The request is an `HttpRequest` with version `HTTP/2.0` and an origin-form URI (absolute-form
on an `h2c` connection or the proxy TLS listener, which are forward proxies). `:authority` becomes `Host`, split `cookie`
fields are joined, and trailers arrive in the `LastHttpContent`. `FlowContext.getStreamId()` is
the stream's id (0 for HTTP/1), and each stream has its own `FlowContext` and timings.
- Towards an HTTP/1.1 server the request becomes HTTP/1.1: the request line says `HTTP/1.1`, `Via`
says `2 `, and a body without a `Content-Length` is sent chunked, with its trailers.
- Responses lose HTTP/1's connection-specific fields (`Connection`, `Keep-Alive`,
`Transfer-Encoding`, `Upgrade`, ...). `Content-Length` is kept, and chunked trailers become a
trailing HEADERS frame; a response with `Content-Length: 0` is a single HEADERS frame that ends
the stream. A `100 Continue` is sent only to a client that asked for one; other 1xx responses
(such as `103 Early Hints`) are forwarded as interim responses.
- Proxy authentication happens once, on the `CONNECT` that started the session. Its streams
inherit it, as HTTP/1 requests inside an intercepted session do. On `h2c` or the proxy TLS listener, each
stream authenticates on its own (`Proxy-Authorization` per request).
### Concurrency
The connection's thread reads frames; each stream runs on a virtual thread of its
own, so a slow stream never holds up the others. Towards HTTP/1.1 servers, which cannot multiplex,
each stream takes a server connection of its own; idle ones are reused by later streams of the
same client connection, and the `CONNECT`'s server connection serves the first stream. Because
streams run at the same time, `ActivityTracker` callbacks and filters for one client connection
may run concurrently, on the streams' threads.
## HTTP/2 to servers
With HTTP/2 to servers on (the default; `withHttp2Upstream(false)`, `--no-http2-upstream` or
`http2_upstream=false` turns it off), the proxy's TLS connections to servers, for intercepted
HTTPS and the `https://` and secure WebSocket requests it
[originates TLS](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-origination-https-uris-without-connect) for, offer `h2` and `http/1.1`
through
ALPN (before `MitmManager.configureServerSocket`, which may change that). An intercepted client
that sent ALPN has its own offer mirrored instead: one that offered only `http/1.1` never gets
HTTP/2 upstream (see [ALPN mirroring](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#alpn-mirroring)). A server that picks `h2`
is spoken to in HTTP/2; any other keeps HTTP/1.1, so turning it on is safe for servers without
HTTP/2. WebSockets use extended `CONNECT` when the origin advertises
`SETTINGS_ENABLE_CONNECT_PROTOCOL`; otherwise they use a separate HTTP/1.1 connection.
Plain-HTTP requests (`http://`
through the proxy, and plain HTTP chained proxies) stay HTTP/1.1, unless they go to a chained
proxy spoken to in HTTP/2 ([HTTP/2 to chained proxies](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#http2-to-chained-proxies));
HTTP/2 inside the tunnel of an HTTP `CONNECT` chained proxy (or through SOCKS) works like a direct
connection.
- **Multiplexing.** An HTTP/2 connection to a server carries many exchanges at once, one stream
each: the streams of an HTTP/2 client, the requests of an intercepted HTTP/1 session, and, with
the shared pool (`withSharedServerConnectionPool` and `withPoolSharedMitmConnections`), the
exchanges of every client connection. A burst of exchanges waits for the connection one of them
is making instead of making their own, and an intercepted `CONNECT` whose requests will use an
existing connection makes none (like a reused pooled connection, it fires no connection hooks).
- **Stream limits.** The proxy reads the server's SETTINGS before the first stream and never opens
more streams than its `SETTINGS_MAX_CONCURRENT_STREAMS`; when every connection to the server is
full, another one is made (counted against the shared pool's limits).
- **Isolation.** Connections are shared only between exchanges with the same target, route
(direct, or which chained proxy) and MITM manager, and between client connections only where a
pooled connection would be: with the shared pool on, and nothing about the connection particular
to one client. A connection that carries a PROXY protocol header, or whose TLS a MITM manager set
up for one client (one that overrides `serverSslContext` or `configureServerSocket` with a
`FlowContext`), serves only its own client connection. Private connections close with their
client; shared ones after the idle timeout without streams. With
[connection coalescing](https://mahmoudimus.github.io/MicroProxy/guides/http2/#connection-coalescing) on, exchanges for other targets may share a
connection too, but never across the rest.
- **Translation.** The request line and `Host` become `:method`, `:scheme https`, `:authority`
and `:path`; connection-specific fields (`Connection` and what it names, `Keep-Alive`,
`Proxy-Connection`, `Transfer-Encoding`, `Upgrade`) are dropped, `cookie` is split into crumbs,
and `te` is sent only as `te: trailers`, when the client sent it. Responses reach filters as
`HTTP/2.0` responses; without a `content-length`, their body is chunked (`Transfer-Encoding:
chunked`), so HTTP/1 clients get trailers too, and they reach HTTP/1 clients with an `HTTP/1.1`
status line and `Via: 2 `.
- **Failures.** A stream the server refuses (`REFUSED_STREAM`) or did not process before its
`GOAWAY` (an id above the last one it names) is retried on another connection when the request
can be sent again (no body, or a buffered one), up to three times. A `GOAWAY` lets the streams
in progress finish and starts no new ones. Any other reset fails the exchange like a broken
server connection: `proxyToServerFailure` with `ProxyFailure.BadServerResponse` and a `502`, or,
once the response has started, a reset client stream (HTTP/2) or a closed connection (HTTP/1).
Connection errors fail every stream of the connection. `PUSH_PROMISE` is refused (push is off).
- **Flow control.** A stream's response data is buffered up to its window (`initialWindowSize`,
256 KiB), which is credited back only as the exchange passes the data on to the client: a slow
client stops its server stream instead of growing the proxy's buffers. The connection's window
(`connectionWindowSize`) is credited back as data arrives, so one slow client does not hold up
the other streams. Request bodies wait for the server's windows.
- **Hooks.** Filters, `proxyToServerConnection*` hooks, timings, `upstreamStatus`, `ActivityTracker`
callbacks, `HttpLogger` and the WARC recorder work per exchange as with HTTP/1.1. A new
connection fires the connection hooks for the exchange that made it; exchanges that take a stream
on an existing one look like exchanges on a reused pooled connection. Bytes sent and received are
counted per stream (each frame to its stream's exchange).
## Connection coalescing
A connection made for `a.example.com` normally carries only `a.example.com`'s exchanges. With
coalescing on, an exchange for `b.example.com` that finds no connection of its own with room may
take a stream on it instead, as RFC 9113 section 9.1.1 allows, when:
- the certificate the server presented on it is valid for `b.example.com`: a DNS subject
alternative name matches it, as the JDK's HTTPS hostname verification matches them (one
wildcard, in the left-most label only; no fallback to the common name); and
- `b.example.com` resolves, with the proxy's own resolver (`withServerResolver`, DNSSEC, and the
`proxyToServerResolution*` filter hooks, as for connecting), to the address the connection went
to; or the server named `https://b.example.com` in an ORIGIN frame (RFC 8336). Once a server has
sent an ORIGIN frame, its origin set (its own name and those it named) says which names the
connection serves, and DNS is no longer asked. Through a chained proxy, which resolves names
itself, only an ORIGIN frame lets a connection serve another name.
| Flag | Property | Bootstrap |
|---|---|---|
| `--http2-coalesce` | `http2_coalesce=true` | `withHttp2Options(Http2Options.builder().coalesceConnections(true).build())` |
It is off by default, and needs HTTP/2 to servers (on unless `--no-http2-upstream`). It applies
to intercepted `CONNECT`s too: a `CONNECT` for a name an existing connection serves makes no connection of its
own, and the certificate the client gets is made from that server's. What the client sees does
not change.
- **Never across isolation.** Connections are coalesced only among those that could be shared
anyway (same route, MITM manager, and client connection for private ones), and only when TLS
would be set up the same way: the MITM manager gives both names the same `SSLContext` (trust,
client certificate, pinned versions and ciphers), the connection sent its own name as SNI (as a
connection for the other name would send that one), the proxy presented no client certificate
on it (an identity given to one name), and the manager does not configure server sockets itself
(`configureServerSocket`, whose settings may differ by name). An IP address target is never
coalesced, nor is a request whose filters chose a TLS identity of its own
([`proxyToServerTlsIdentity`](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#server-name-and-verification-host)): it goes only on
connections made with that identity.
- **Resolving once.** The address check resolves the name the way connecting would, and a request
that goes on to make its own connection connects to that address, without resolving it again.
Names a connection was found to serve are remembered for its lifetime, so later requests take a
stream at once. With coalescing off, nothing more is resolved than before.
- **`421 Misdirected Request`.** A server that answers `421` to a request on a coalesced
connection is not sent that name on that connection again. The request goes again on a
connection of its own when it can be: no body, a buffered one, or one the server refused before
it was read (`Expect: 100-continue`). A request whose body was already streamed gets the `421`,
as a client would from the server. A `421` on a connection made for the request's own name is
relayed as always.
- **Stats.** `http2.upstream.coalesced_streams` counts the streams opened on a connection made for
another name, and `http2.upstream.misdirected_421_retry` the requests sent again after a `421`
(also counted in their cluster's `upstream_rq_retry`). A coalesced stream's requests count in
its own name's cluster (with `--stats-per-host`), its connection in the other's.
## Trailers and gRPC
Trailers go end to end: request trailers from HTTP/2 clients (and from HTTP/1 clients' chunked
bodies) reach HTTP/2 servers as a trailing HEADERS frame, and response trailers such as
`grpc-status` and `grpc-message` come back to HTTP/2 clients the same way, and to HTTP/1 clients
in the chunked body. When both the client and the server side of an exchange are HTTP/2 streams
and no filter looks at the request body's pieces, the request body is relayed on a thread of its
own while the response comes back, so bidirectional streaming works; otherwise a request body is
sent whole before the response is read, as with HTTP/1.1. So gRPC (`application/grpc`, `te:
trailers`, length-prefixed messages, unary and streaming calls, Trailers-Only error responses)
works through the intercepting proxy with `--mitm` (HTTP/2 being on by default both ways). To
read the calls, log them with [content views](https://mahmoudimus.github.io/MicroProxy/guides/content-views/#content-views) (`--log-http-view auto`), which decode each
message and the status trailers; scripts can decode and rewrite messages with the
[`protobuf` and `grpc` modules](https://mahmoudimus.github.io/MicroProxy/guides/scripting/).
## gRPC interop (grpcio)
`GrpcInteropTest` runs real gRPC peers on either side of the proxy: grpcio's client and server
(the scripts in `microproxy/src/test/resources/interop/`, with generic handlers on raw bytes, so
no generated code). The client reaches the server through the proxy as a forward proxy, with
`CONNECT` (grpcio's `grpc.http_proxy`), and trusts the proxy's MITM CA; the proxy speaks HTTP/2 on
both sides, as with `--mitm` and the HTTP/2 defaults, and trusts the server's CA. The calls:
- unary, with text and binary metadata both ways;
- server, client and bidirectional streaming, each bidirectional message answered while the
request stream stays open;
- an error: `INVALID_ARGUMENT` with its message and a `google.rpc.Status` in
`grpc-status-details-bin`, decoded on arrival;
- a deadline: `grpc-timeout` reaches the server, which sees it and ends the call;
- a cancellation: the client's `RST_STREAM` `CANCEL` reaches the server, whose handler sees the
call end;
- large messages, far across the flow-control windows of both hops: 8 MiB each way, 4 x 2 MiB
streamed from the server, and 6 MiB and 1 MiB streamed to it.
They run as they are, through a Java `FrameInterceptor` that passes every frame on unchanged
(and checks that the deadline, the cancellations and the trailers crossed both hops), and, in
`microproxy-starlark`'s `ScriptedGrpcInteropTest`, through an `on_frame` script that does the
same. CI runs them on every push and pull request (the `test` job of
[`.github/workflows/ci.yml`](https://github.com/mahmoudimus/MicroProxy/blob/main/.github/workflows/ci.yml), on JDK 21 and 25, with `grpcio==1.84.0`
and `-Dgrpc.interop.required=true`, so a missing grpcio fails the build). Elsewhere they
are skipped unless Python has grpcio:
```bash
python3 -m pip install grpcio==1.84.0
mvn -pl microproxy,microproxy-starlark -am verify -Dtest=GrpcInteropTest,ScriptedGrpcInteropTest \
-Dsurefire.failIfNoSpecifiedTests=false -Dgrpc.interop.python=python3
```
## h2c with prior knowledge
With `withHttp2Cleartext(true)` (`--http2-cleartext`, `http2_cleartext=true`), a connection to the
plain listener whose first bytes are the HTTP/2 connection preface (`PRI * HTTP/2.0`, RFC 9113
section 3.4) is served as HTTP/2. The first bytes are compared without consuming anything, and an
HTTP/1 request is told apart by its first byte or two, so HTTP/1 clients are unaffected. Its
streams are proxy requests: `:scheme` and `:authority` name the target, so any authority is valid
(the `421` check applies only to intercepted sessions), and they reach servers as HTTP/1.1 (HTTP/2
to servers needs TLS). A `:scheme https` stream, a secure WebSocket's extended `CONNECT`
included, is fetched over TLS the proxy opens to the server ([TLS
origination](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-origination-https-uris-without-connect)), and may use HTTP/2 or HTTP/3 there.
`Upgrade: h2c` (deprecated by RFC 9113) is ignored, as
before: the request is answered in HTTP/1.1. The same limits as for intercepted HTTP/2 apply.
### h2c-only listener
With `withHttp2CleartextOnly(true)` (`--http2-cleartext-only`, `http2_cleartext_only=true`), the
plain listener serves `h2c` with prior knowledge and nothing else, for a listener only HTTP/2
clients use, such as a reverse proxy in front of gRPC servers. A connection whose first bytes are
not the preface is a connection error of type `PROTOCOL_ERROR` (RFC 9113 section 3.4): the proxy
sends its `SETTINGS` and a `GOAWAY` with `PROTOCOL_ERROR`, and closes it, where a shared listener
would serve it as HTTP/1. It implies `withHttp2Cleartext`, and a proxy with it fails to start with
a TLS listener, SOCKS, transparent mode or a `tcp://` reverse proxy, which all take connections
that are not HTTP/2. The admin API's `/listeners` shows the listener as `h2c` alone.
```bash
java -jar microproxy-starlark-all.jar --port 8080 --mode reverse:https://grpc.example.com --http2-cleartext-only
```
## CONNECT and WebSockets
A normal HTTP/2 `CONNECT` (`:method CONNECT`, `:authority host:port`, no `:scheme` or `:path`)
opens a byte tunnel on that stream. The successful `200` HEADERS leave it open; DATA carry tunnel
bytes, and END_STREAM half-closes one direction. RST_STREAM closes the tunnel's origin connection
and wakes its waits, without closing the client HTTP/2 connection or other streams. A reset that
ends an open tunnel is not counted as a rapid reset. A `CONNECT` stream to a chained HTTP proxy
reaches it as an HTTP/1.1 `CONNECT`.
With a MITM manager, a `CONNECT` stream on `h2c` or the proxy TLS listener is intercepted as an
HTTP/1 `CONNECT` is: the client's TLS session runs over the stream's DATA frames, and its decrypted
requests (HTTP/1.1, or HTTP/2 inside the stream when the client negotiates `h2`) are exchanges of a
client connection of their own, with the same filters, hooks and server connections; the
`CONNECT`'s credentials cover them. The stream's first bytes decide as an HTTP/1 connection's do:
its ClientHello goes through the host rules and the ClientHello hooks, ALPN is mirrored, and plain
HTTP or another protocol is served or tunnelled
([Deciding what to intercept](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)). A server that does not speak
TLS gets a plain tunnel instead. A
`CONNECT` inside an intercepted session, for any authority, goes to the intercepted server, which
the client takes for its HTTPS proxy ([In front of an HTTP or HTTPS proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy)).
The proxy advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL = 1`. A WebSocket client can open a stream
with `:method CONNECT`, `:protocol websocket`, `:scheme http` (ws) or `https` (wss), `:authority`
and `:path`. A successful response is `200`, without END_STREAM; WebSocket frames travel in DATA,
independently of DATA frame boundaries. HTTP/1 `Connection` and `Upgrade` fields remain forbidden
on the HTTP/2 wire. On a forward proxy that serves CONNECT-UDP (`--masque`, or `--http3`),
`:protocol connect-udp` opens a UDP flow whose datagrams travel in capsules in the stream's DATA
([CONNECT-UDP](https://mahmoudimus.github.io/MicroProxy/guides/masque/)); elsewhere it gets `501`, and so do other extended protocols.
WebSockets can use HTTP/1 or HTTP/2 independently on either side. With HTTP/2 to origins on (the
default), a TLS origin that advertises extended CONNECT gets a WebSocket stream on the existing shared
HTTP/2 connection. An origin that does not advertise it gets a separate HTTP/1.1 upgrade connection.
The proxy translates `101` and `200` handshakes, and generates the HTTP/1 nonce/accept fields where
needed. For secure extended CONNECT on a forward-proxy connection, origin TLS uses the configured
MITM manager's server TLS settings, or the default JVM trust context when there is no manager, as
for any `https://` request the proxy [originates TLS](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-origination-https-uris-without-connect)
for.
Request filters see an extended WebSocket CONNECT as a GET upgrade request, with version
`HTTP/2.0`, its resource URI and generated HTTP/1 upgrade headers. Response filters and trackers
see the handshake's answer as a `101` with `Upgrade: websocket` and `Connection: Upgrade`, as for an
HTTP/1.1 client, whichever protocol the origin speaks; the proxy sends it to an HTTP/2 client as
`200`, and `upstreamStatus` retains the origin's actual status.
`webSocketFrameReceived`, `filterWebSocketFrame` and `filterWebSocketMessage` use the same parser
and relay as HTTP/1, including masking, rewriting, dropping, fragmentation, message joining,
permessage-deflate and the buffer and message limits (see [WebSocket frames and
messages](https://mahmoudimus.github.io/MicroProxy/guides/websockets/#websocket-frames-and-messages)).
A filter that rejects the handshake finishes the exchange without starting the relay.
### Remaining limits
A request for an authority other than the intercepted one gets `421
Misdirected Request`, which makes the client retry on a connection of its own; so does an extended
`CONNECT` (`:protocol websocket`) for another authority. A plain `CONNECT`, and a `:scheme http`
request, are proxy requests, forwarded instead ([In front of an HTTP or HTTPS proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy)). On a
forward-proxy HTTP/2 connection (`h2c`, or the proxy's TLS listener), a `:scheme https` request is
fetched over TLS the proxy opens itself ([TLS origination](https://mahmoudimus.github.io/MicroProxy/guides/forward-proxy/#tls-origination-https-uris-without-connect)),
as an absolute `https://` URI sent in plain HTTP is. Towards a chained proxy, the proxy may speak
HTTP/2 too ([HTTP/2 to chained proxies](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#http2-to-chained-proxies)).
## Limits
Input is untrusted; every limit has a default, and the configurable ones are set with
`Http2Options` (or the `http2_*` properties and `--http2-max-streams`). Rate limits count frames
per window of 10 seconds (`rateWindow`). The window sizes apply to both sides: what the proxy
buffers of a client's request bodies, and of a server's responses.
| Limit | Default | When exceeded | Setting |
|---|---|---|---|
| Open streams per connection | 100 | the stream is refused (`REFUSED_STREAM`); clients retry | `maxConcurrentStreams` |
| Request bytes buffered per stream | 256 KiB (its flow-control window) | stream reset `FLOW_CONTROL_ERROR` | `initialWindowSize` |
| Request bytes buffered per connection | 1 MiB (the connection window) | `GOAWAY FLOW_CONTROL_ERROR` | `connectionWindowSize` |
| Request header list (names and values plus 32 per field) | `max_header_size` + `max_initial_line_length` (24 KiB) | stream reset | `maxHeaderListSize` |
| Encoded header block (with CONTINUATION frames) | 64 KiB, or the header list limit plus 4 KiB if larger | `GOAWAY ENHANCE_YOUR_CALM` | |
| CONTINUATION frames per header block | 128 | `GOAWAY ENHANCE_YOUR_CALM` | |
| HPACK dynamic table / frame size | 4096 / 16384 bytes | `GOAWAY COMPRESSION_ERROR` / `FRAME_SIZE_ERROR` | |
| Streams reset by the client before their response completed, or refused (Rapid Reset, CVE-2023-44487) | 100 per window | `GOAWAY ENHANCE_YOUR_CALM` | `maxRapidResets` |
| PING / SETTINGS frames | 100 / 100 per window | `GOAWAY ENHANCE_YOUR_CALM` | `maxPings` / `maxSettings` |
| RST_STREAM and PRIORITY frames | 1000 per window | `GOAWAY ENHANCE_YOUR_CALM` | `maxResets` |
| WINDOW_UPDATE frames | 10000 per window | `GOAWAY ENHANCE_YOUR_CALM` | `maxWindowUpdates` |
| Empty DATA frames (no data, no END_STREAM) | 1000 per window | `GOAWAY ENHANCE_YOUR_CALM` | `maxEmptyFrames` |
| Acknowledging the proxy's SETTINGS | 10 seconds | `GOAWAY SETTINGS_TIMEOUT` | `settingsAckTimeout` |
| A connection with no open streams | `idle_connection_timeout` (70 s) | `GOAWAY NO_ERROR`, then closed | `withIdleConnectionTimeout` |
| A stream waiting for request data or for flow-control window, a write the client does not read | `idle_connection_timeout` | the stream is reset / the connection closed | `withIdleConnectionTimeout` |
| Response bytes buffered per server stream | 256 KiB (its window) | the server waits for window | `initialWindowSize` |
| A connection to a server with no streams, a server that sends nothing for a stream, a write it does not read | `idle_connection_timeout` | `GOAWAY NO_ERROR` and closed / `504` / the connection closed | `withIdleConnectionTimeout` |
Protocol violations end the connection with `GOAWAY` and the error RFC 9113 names: a bad preface,
frames other than SETTINGS first, even or decreasing stream ids (`PROTOCOL_ERROR` /
`STREAM_CLOSED`), frames on idle streams, `PUSH_PROMISE` from a client. Malformed requests
(missing or repeated pseudo-headers, upper-case or connection-specific fields, a `content-length`
the DATA does not match, a stream that depends on itself) only reset their stream, as do frames a
client sends on a stream after resetting it (`STREAM_CLOSED`). Request bodies are buffered only
within the flow-control windows the proxy granted, so a connection holds at most its connection
window of request data, plus two 16 KiB I/O buffers and its HPACK tables. Malformed responses
from servers reset their stream and fail its exchange with a `502`.
## Shutting down
A graceful stop (`stop()`) sends `GOAWAY` to HTTP/2 clients, finishes the
streams already open (for up to the graceful stop timeout), and refuses new ones. An idle
connection is sent `GOAWAY` and closed. Connections to servers are sent `GOAWAY` and closed once
the client connections are done.
## Compatibility
With HTTP/2 off (`--no-http2 --no-http2-upstream`, or without the codec), nothing changes from
HTTP/1-only behaviour, with two small exceptions in the API: `FlowContext.equals` (and
`hashCode`) now also compare the stream id, which is 0 for every HTTP/1 context, so HTTP/1 behaviour is the same; and `HttpProxyServerBootstrap` gains
`withHttp2`, `withHttp2Upstream`, `withHttp2Cleartext`, `withHttp2CleartextOnly`,
`withHttp2Options` and `getHttp2Options` as default methods, so other implementations still compile. Trackers and filters keyed by
`FlowContext` keep one entry per stream; state meant per client connection can be keyed by
`ctx.getConnectionContext()`.
## Conformance (h2spec)
CI runs [h2spec](https://github.com/summerwind/h2spec) v2.6.0 in strict mode against the `h2c`
listener twice (`.github/scripts/h2spec.sh`, with `org.microproxy.H2specTarget` from the test
sources: the proxy with `h2c` on and a filter that answers every request with `200`):
- **The [h2c-only listener](https://mahmoudimus.github.io/MicroProxy/guides/http2/#h2c-only-listener)** (`-Dh2spec.cleartextOnly=true`): all 147 cases
pass.
- **The listener shared with HTTP/1** (`--http2-cleartext`): all 147 cases pass except one, which
this run leaves out. `http2/3.5/2` (*Sends invalid connection preface*) expects `GOAWAY` or a
closed connection, but on a shared listener a connection that does not start with the preface
is an HTTP/1 connection, and gets an HTTP/1 error response before it is closed.
To run it locally, build the classes (`mvn -DskipTests test-compile -pl microproxy -am`) and run
`.github/scripts/h2spec.sh --strict` (it downloads h2spec, or uses `$H2SPEC`), with
`H2SPEC_TARGET_OPTS=-Dh2spec.cleartextOnly=true` for the h2c-only listener.
HTTP/3 has its own section: [HTTP/3 and QUIC](https://mahmoudimus.github.io/MicroProxy/guides/http3/).
# HTTP/3 and QUIC
Serve HTTP/3 over the proxy's own QUIC transport to the clients of a reverse proxy and of a forward
proxy's TLS listener, and speak HTTP/3 to servers that offer it, with session resumption, 0-RTT,
`Alt-Svc` handling and hardened limits. Use this page to turn HTTP/3 on; QUIC that clients redirect
to the proxy is in [Transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic), UDP proxying in
[CONNECT-UDP (MASQUE)](https://mahmoudimus.github.io/MicroProxy/guides/masque/), and QUIC that is not HTTP/3 in [QUIC stream
hooks](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/).
MicroProxy speaks HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)) over its own
QUIC transport, the [`quic`](https://github.com/mahmoudimus/MicroProxy/blob/main/quic/README.md) module (ported from quinn and rustls), with
[`http3-codec`](https://github.com/mahmoudimus/MicroProxy/blob/main/http3-codec/README.md) for frames and QPACK. It does so in four places: to the
clients of a reverse proxy, to the clients of a [forward proxy](https://mahmoudimus.github.io/MicroProxy/guides/http3/#an-http3-forward-proxy-listener)
that serves it next to its TLS listener (for `CONNECT`, WebSockets and MASQUE), to clients whose
QUIC is redirected to a [transparent proxy](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic) or carried in a
[CONNECT-UDP flow](https://mahmoudimus.github.io/MicroProxy/guides/masque/#quic-inside-connect-udp-flows), and to origin servers known to support it. QUIC
that carries another protocol can be intercepted too, its streams relayed through
[hooks](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/). All need the optional
`quic` and `http3-codec` jars on the class path (the `microproxy-starlark` `-all` jar bundles
them); a proxy started with HTTP/3 enabled without them fails at startup with an
`IllegalStateException` that says so, and without HTTP/3 nothing loads them.
```java
MicroProxy.bootstrap()
.withReverseProxy("https://backend.internal") // --mode reverse:https://backend.internal
.withSslContextSource(new KeyStoreSslContextSource(Path.of("site.p12"), password)) // --listener-keystore site.p12
.withHttp3(true) // to clients: --http3, http3=true
.withHttp3Upstream(true) // to servers: --http3-upstream, http3_upstream=true
.withHttp3UpstreamHosts("\\.internal:") // --http3-upstream-hosts, http3_upstream_hosts
.withHttp3Options(Http3Options.builder().maxConcurrentStreams(200).build()) // optional
.start();
```
```bash
java -jar microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8443 --mode reverse:https://backend.internal \
--listener-keystore site.p12 --listener-keystore-password secret --http3 --http3-upstream
```
## Serving HTTP/3 in reverse mode
With `--http3` (`withHttp3(true)`, `http3=true`) and `--mode reverse:...`, the proxy opens a UDP
listener on the TCP listener's port number and serves HTTP/3 there (ALPN `h3`).
- **Certificate.** The HTTP/3 listener presents the TLS listener's certificate: its
`SslContextSource` gives it as `KeyMaterial` (`getKeyMaterial`), which
`KeyStoreSslContextSource` (`--listener-keystore`, `listener_keystore`) and
`SelfSignedSslContextSource` do; QUIC runs its own TLS 1.3 handshake and needs the key itself,
not an `SSLContext`. Without a TLS listener, a `CertificateAuthorityMitmManager` (`--mitm`)
issues one for the name the client asks for. With neither, the proxy fails at startup.
- **Exchanges.** Each request stream is an exchange like any HTTP/1 or HTTP/2 request, on a
virtual thread of its own: filters, the cache, authentication (per stream, as on `h2c`),
logging, WARC and HAR recording, timings and trackers all apply. The request is an
`HttpRequest` with version `HTTP/3.0` (`HttpVersion.HTTP_3_0`), `FlowContext.getHttpVersion()`
says `HTTP/3.0` and `getStreamId()` gives the QUIC stream id, and scripts see
`req.http_version == "HTTP/3"`. The upstream gets it as HTTP/1.1, HTTP/2 (unless `--no-http2-upstream`)
or HTTP/3 (`--http3-upstream`), whichever it speaks: a body without `content-length` goes chunked
to an HTTP/1.1 server, trailers go through end to end where both sides carry them, and
`100-continue` and `103 Early Hints` reach the client as interim responses.
- **Advertising.** Responses sent over TCP get `Alt-Svc: h3=":"; ma=86400`, so that clients
find the HTTP/3 listener, unless the server's own `Alt-Svc` field is left after the `h3`
stripping (below), which then goes to the client as it is; a `proxyToClientResponse` filter may
remove the field. `Http3Options.altSvc(false)` (`--no-http3-alt-svc`, `http3_alt_svc=false`)
turns it off, and `altSvcMaxAge` sets `ma`. Browsers act on `Alt-Svc` only over HTTPS, so pair
HTTP/3 with a TLS listener.
- **WebSockets** over extended `CONNECT` (RFC 9220) reach the upstream as described
[below](https://mahmoudimus.github.io/MicroProxy/guides/http3/#websockets-over-http3); `CONNECT` is refused with `400`, as over HTTP/1 and HTTP/2, and
CONNECT-UDP with `501`. There is no server push, and no HTTP datagrams (SETTINGS_H3_DATAGRAM is
not sent, nor QUIC DATAGRAM frames accepted).
### Limits
**Limits** (of every HTTP/3 connection to clients). Much of HTTP/2's defence is QUIC's in HTTP/3: the stream limit and flow control are
the transport's, which a peer cannot exceed without a QUIC error. The proxy's own limits are set
with `Http3Options` (or `http3_*` properties and `--http3-max-streams`); rate limits count per
window of 10 seconds (`rateWindow`).
| Limit | Default | When exceeded | Setting |
|---|---|---|---|
| Request streams open per connection | 100 | QUIC holds new streams back until one ends | `maxConcurrentStreams` |
| Bytes a peer sends ahead of the proxy, per stream / per connection | 256 KiB / 1 MiB (QUIC flow control) plus 64 KiB read ahead per stream | QUIC `FLOW_CONTROL_ERROR` | `streamReceiveWindow` / `connectionReceiveWindow` |
| Field section (names and values plus 32 per field), SETTINGS_MAX_FIELD_SECTION_SIZE | `max_header_size` + `max_initial_line_length` (24 KiB) | `431` for a request (the connection goes on), `502` for a response | `maxFieldSectionSize` |
| QPACK dynamic table, each way | 4096 bytes | `QPACK_ENCODER_STREAM_ERROR` | `qpackMaxTableCapacity` |
| Streams blocked on the peer's QPACK encoder stream | 16, each for at most `idle_connection_timeout` | `QPACK_DECOMPRESSION_FAILED` / the stream is rejected | `qpackBlockedStreams` |
| Frames of unknown and reserved types, and empty DATA frames | 1000 per window | `H3_EXCESSIVE_LOAD` | `maxUnknownFrames` |
| Unidirectional streams of unknown and reserved types | 100 per window (each abandoned with `STOP_SENDING`) | `H3_EXCESSIVE_LOAD` | `maxUnknownStreams` |
| Requests reset by the client before their response completed (Rapid Reset) | 100 per window | `H3_EXCESSIVE_LOAD` | `maxRapidResets` |
| A connection with nothing to do | `idle_connection_timeout` (70 s) | QUIC's idle timeout closes it | `withIdleConnectionTimeout` |
| Client address validation (QUIC Retry) | off | | `retry` |
### Congestion control
The proxy's QUIC connections, those
its HTTP/3 and transparent QUIC listeners accept and those it opens to servers, use CUBIC by
default. `--quic-congestion newreno` or `bbr` (`quic_congestion`,
`Http3Options.quicCongestionControl`) chooses NewReno or BBR. BBR, ported from quinn and
experimental there too, paces at its estimate of the path's bandwidth rather than backing off on
every loss, so it keeps its throughput on paths with random loss (wireless links, long distances)
where CUBIC collapses; it is not tuned for sharing a bottleneck fairly. See the [quic
module](https://github.com/mahmoudimus/MicroProxy/blob/main/quic/README.md#connection-internals) for its behaviour and limits.
Protocol violations close the connection with the code RFC 9114 or RFC 9204 names: a second
control or QPACK stream or a push stream from a client (`H3_STREAM_CREATION_ERROR`), frames out of
place (`H3_FRAME_UNEXPECTED`, `H3_MISSING_SETTINGS`), a critical stream that ends
(`H3_CLOSED_CRITICAL_STREAM`), bad settings (`H3_SETTINGS_ERROR`), bad QPACK. Malformed requests
(missing or repeated pseudo-headers, upper-case or connection-specific fields, a
`content-length` the DATA does not match) only reset their stream, with `H3_MESSAGE_ERROR`.
### Shutting down
A graceful stop (`stop()`) sends `GOAWAY` naming the first stream the proxy
will not process; streams opened after it are refused with `H3_REQUEST_REJECTED`, which clients
retry elsewhere, the streams in progress finish (for up to the graceful stop timeout), and the
connection closes once the client has closed it, or after two seconds.
## An HTTP/3 forward-proxy listener
With `--http3` (`withHttp3(true)`, `http3=true`) on a forward proxy whose own TLS listener is on
(`--listener-keystore`, `withSslContextSource`), the proxy also serves HTTP/3 on UDP at the TCP
listener's port, with the same certificate. Browsers send HTTP/3 only to origins, never to a
proxy; this listener is for clients configured to use an HTTP/3 proxy, MASQUE clients first:
they tunnel TCP with `CONNECT`, WebSockets with extended `CONNECT`, and UDP (QUIC included) with
CONNECT-UDP, all over one QUIC connection to the proxy.
- **Proxy requests.** A request names its target with `:scheme` and `:authority`, and goes on as
an HTTP/1 client's absolute-form proxy request does: filters see `http://host:port/path` with
version `HTTP/3.0`. Every authority is served (no `421`: a forward proxy's requests name their
targets), and no `Alt-Svc` is added (a forward proxy's responses are its servers').
- **Proxy authentication** applies per stream, as on `h2c`: a stream without valid credentials
gets `407` with `proxy-authenticate`, and the connection goes on.
- **SETTINGS.** The listener sends SETTINGS_ENABLE_CONNECT_PROTOCOL and SETTINGS_H3_DATAGRAM,
and accepts QUIC DATAGRAM frames (the `max_datagram_frame_size` transport parameter). The
reverse proxy's listener and intercepted transparent QUIC send neither datagram setting and
refuse DATAGRAM frames.
- **Not served.** A forward proxy without a TLS listener, and a transparent proxy (whose clients'
QUIC goes to `--transparent-quic-port`), fail at startup with `--http3`.
```java
MicroProxy.bootstrap()
.withPort(8443)
.withSslContextSource(new KeyStoreSslContextSource(Path.of("proxy.p12"), password)) // --listener-keystore
.withHttp3(true) // --http3: UDP 8443 too
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca)) // intercept CONNECT streams (optional)
.withHttp3Options(Http3Options.builder()
.connectUdpIdleTimeout(Duration.ofSeconds(30)) // --masque-udp-idle-timeout 30
.connectUdpMaxFlows(4096, 64) // --masque-max-udp-flows(-per-connection)
.connectUdpReleaseWait(Duration.ofMillis(250)) // --masque-release-wait 250
.build())
.start();
```
## CONNECT over HTTP/3
A `CONNECT` stream (RFC 9114 section 4.4: `:method CONNECT` and `:authority host:port` only)
opens a byte tunnel, as an HTTP/2 `CONNECT` stream does ([CONNECT and
WebSockets](https://mahmoudimus.github.io/MicroProxy/guides/http2/#connect-and-websockets)): its `200` leaves the stream open, DATA frames carry the
tunnel's bytes, and the stream's FIN half-closes one direction. A reset ends the tunnel and
closes its server connection, and is not counted as a rapid reset; a server connection that fails
resets the stream with H3_CONNECT_ERROR. HEADERS after the `CONNECT`'s are a connection error
(H3_FRAME_UNEXPECTED).
The stream goes the way an HTTP/1 or HTTP/2 `CONNECT` does, because it is shown to the same code
as a socket: with a MITM manager, the stream's first bytes decide ([Deciding what to
intercept](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#deciding-what-to-intercept-the-clienthello)): a ClientHello goes through the host
rules and the ClientHello hooks, and an intercepted session is a client connection of its own
inside the stream (HTTP/1.1, or HTTP/2 when the client negotiates `h2`), whose requests filters
see; plain HTTP is served as HTTP; anything else is tunnelled, through the [TCP
hooks](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/#raw-tcp-streams) and `--tcp-hosts`. Chained proxies and NO_PROXY apply as to any
`CONNECT`. Inside an intercepted HTTP/3 session (transparent QUIC), a `CONNECT` goes to the
intercepted server, which the client takes for its HTTPS proxy ([In front of an HTTP or HTTPS proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy)),
and a request for another host gets `421`.
### WebSockets over HTTP/3
**WebSockets over HTTP/3** (RFC 9220) work on every HTTP/3
connection to clients, as over HTTP/2: an extended `CONNECT` with `:protocol websocket`, `:scheme`
`http` or `https`, `:authority` and `:path` becomes the HTTP/1.1 upgrade an HTTP/1.1 client would
send. Filters see a `GET` with `Upgrade: websocket` (absolute-form on a forward proxy) and the
answer as a `101`, which the client gets as a `200`; the frame and message hooks, the WebSocket
limits and permessage-deflate apply as for HTTP/1 and HTTP/2 ([WebSocket frames and
messages](https://mahmoudimus.github.io/MicroProxy/guides/websockets/#websocket-frames-and-messages)). Towards the server the WebSocket goes as an HTTP/1.1
upgrade, as an extended `CONNECT` to an HTTP/2 server that enables RFC 8441, or, with
`--http3-upstream`, as an extended `CONNECT` on the shared QUIC connection to an HTTP/3 server
whose SETTINGS enable it. A server that resets the connection cancels the stream
(H3_REQUEST_CANCELLED). Other `:protocol`s get `501`.
```python
# aioquic's H3Connection, a WebSocket through the proxy's HTTP/3 listener:
http.send_headers(stream_id, [(b":method", b"CONNECT"), (b":protocol", b"websocket"),
(b":scheme", b"https"), (b":authority", b"chat.example:443"),
(b":path", b"/socket"), (b"sec-websocket-version", b"13")])
```
## Session resumption and 0-RTT
The proxy's QUIC servers that serve HTTP/3 (`--http3`, the clients the [transparent
QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic) listener intercepts as HTTP/3, and HTTP/3 intercepted inside CONNECT-UDP
flows with `--masque-intercept-quic`) send each client two TLS session tickets after its
handshake, sealed with a key of the proxy's that rotates every six hours, and two NEW_TOKEN tokens.
A returning client resumes its session (no certificate exchange: a fresh key exchange and a PSK)
and skips the Retry round trip of `--http3-retry`. Tickets work across all the QUIC servers of one
proxy, but not across proxy processes: each has its own key. QUIC intercepted stream by stream
(the [QUIC stream hooks](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/)) gets no tickets, so no resumption and no 0-RTT: its
streams are opaque, and the proxy could not tell what in them is safe to replay.
### 0-RTT to the proxy
**0-RTT to the proxy** is off by default. `--http3-0rtt` (`Http3Options.zeroRtt(true)`,
`http3_0rtt=true`) lets a resuming client send its first requests as early data, before its
handshake completes, saving a round trip. Early data is not protected against replay: whoever
captures a client's first flight can send it again, and the proxy would act on it twice ([RFC
8446 section 8](https://datatracker.ietf.org/doc/html/rfc8446#section-8)). So:
- **The QUIC layer** accepts a ticket's early data at most once, and only while the ticket age the
client reports agrees with the proxy's within 10 seconds, which a stale capture fails. Its record
of used tickets is in memory, one per process that all its QUIC servers share, and bounded
(refusing early data when full); rejected early data costs the client nothing but the round
trip, as it sends it again.
- **The proxy** treats a request any part of which arrived as early data as
[RFC 8470](https://datatracker.ietf.org/doc/html/rfc8470) says. A method that is not safe to
repeat (anything but `GET`, `HEAD` and `OPTIONS` without a body, and so any `CONNECT`, extended
or not) gets `425 Too Early`, and the client sends it again once its handshake completes. Others go on with `Early-Data: 1`, so the
server (or another proxy behind) can answer `425` itself, which the proxy relays.
- **Filters** see `FlowContext.isEarlyData()` (`ctx.early_data` in scripts) and may answer `425`
to requests they deem unsafe, `GET`s with side effects for instance:
```python
def on_request(req, ctx):
if ctx.early_data and "/checkout" in req.uri:
return response(425, "too early\n")
```
The proxy does not hold early requests back until the handshake completes; one that arrived early
is marked even when the handshake has completed by the time it goes on.
### 0-RTT to servers
**0-RTT to servers** is on with `--http3-upstream` (`Http3Options.upstreamZeroRtt`,
`http3_upstream_0rtt`; `--no-http3-upstream-0rtt` turns it off). The proxy keeps the tickets
servers send, per server name, for as long as it runs, and resumes when it opens a new QUIC
connection to one. If the request that opens the connection is safe to replay (`GET`, `HEAD`,
`OPTIONS` without a body) and the ticket allows it, the request goes as 0-RTT data; requests that
are not wait for the handshake. A server that rejects the early data gets it again in 1-RTT
packets; one that answers `425 Too Early` gets the request again after the handshake, unless the
request itself arrived as early data (`Early-Data: 1`), whose `425` goes back to its client.
## HTTP/3 to servers
With `--http3-upstream` (`withHttp3Upstream(true)`, `http3_upstream=true`), the proxy speaks
HTTP/3 to servers known to support it, whatever protocol the client spoke (HTTP/1, HTTP/2 or
HTTP/3), in any mode.
- **Knowing.** A server is known to speak HTTP/3 once one of its responses over TLS advertised
`h3` on its own host (`Alt-Svc: h3=":443"`), remembered for the advertisement's `ma` (24 hours
without one) and forgotten on `Alt-Svc: clear` or `ma=0`. The proxy learns before it strips
`h3` for the client, so stripping stays on. Alternatives on other hosts, and draft versions
(`h3-29`), are never used. `--http3-upstream-hosts ` (`withHttp3UpstreamHosts`,
`http3_upstream_hosts`) names servers to reach over HTTP/3 at once, on their URI's port.
- **Sharing.** One QUIC connection per server, shared by every exchange for it, of every client,
as streams, within the server's stream limit. Nothing about a QUIC connection is particular to
one client.
- **Trust.** Servers are validated as TLS connections to them are: with the MITM manager's
server trust (`MitmManager.serverTrustManager`, which `CertificateAuthorityMitmManager` and
every context from `SslContexts` provide, so `--mitm-trust-all` works too), or the JDK's default
trust store without a manager, checking the host name as HTTPS does. A manager whose trust the
proxy cannot reproduce keeps its servers on TCP.
- **Falling back** (happy eyeballs, lite). A QUIC handshake that fails, or does not complete
within `upstreamHandshakeTimeout` (1 second), sends the exchange over TCP, and the server stays
on TCP for `upstreamFailureTimeout` (5 minutes). Exchanges for the same server wait for a
handshake in progress rather than start their own.
- **Failures.** A `GOAWAY` from the server, or a stream it refuses with `H3_REQUEST_REJECTED`,
makes the request safe to send again, on a new connection. QUIC's idle timeout answers `504`
(`ProxyFailure.ServerTimeout`); other resets and connection errors `502`
(`ProxyFailure.BadServerResponse`), or reset the client's stream once the response has started.
- **Only direct connections.** Requests through chained proxies (UDP does not go through them),
through a tunnel a session's server opened, with a PROXY protocol header, and proxy requests
keep using TCP.
- **WebSockets** go over HTTP/3 to a server whose SETTINGS enable extended `CONNECT` (RFC 9220),
waited for up to `upstreamHandshakeTimeout`; to other servers on an HTTP/1.1 connection of their
own.
## Alt-Svc: which clients may move to HTTP/3
HTTP/3 runs over QUIC, on UDP. Browsers never send QUIC through an HTTP proxy, and a transparent
setup may redirect only TCP. So a client that learns that an origin speaks HTTP/3 can switch to it
and bypass the proxy, and with it interception, filters and logging, unless its UDP is redirected
to a [transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic) port. Clients learn about HTTP/3 in two ways:
- **From `Alt-Svc` response headers** such as `Alt-Svc: h3=":443"; ma=86400, h2=":443"`. The
proxy removes the `h3` alternatives, including the draft versions (`h3-29`, `h3-Q050`, ...)
and Google QUIC's `quic`. Other alternatives such as `h2=":443"` are kept with their
parameters (`ma`, `persist`), a header left empty is removed, and `Alt-Svc: clear` is passed
on. This is **on by default with interception (`--mitm`, `withManInTheMiddle`) or
`--transparent`**, and off for a plain forward proxy; **with `--transparent-quic-port`, it is
off by default for clients whose traffic was redirected to the proxy**, whose QUIC reaches the
proxy too (table below). `--no-alt-svc-h3`, `withoutHttp3Advertisement()` or
`strip_alt_svc_h3=true` turn it on for every client, and `--keep-alt-svc-h3`,
`withAltSvcH3Stripping(false)` or `strip_alt_svc_h3=false` off for every client. Filters see server
responses unchanged in `serverToProxyResponse` and already stripped in
`proxyToClientResponse`. Short-circuit responses (including cache answers) are stripped as
they are sent. With `--http3-upstream`, the proxy itself learns from the advertisement first.
- **From DNS `HTTPS` records**, which the proxy never sees. In transparent deployments without
a transparent QUIC port, **block UDP port 443** (for example `iptables -A FORWARD -p udp
--dport 443 -j REJECT`). Clients then fall back to TCP, which the proxy does see.
Who keeps `h3` alternatives, by default (an explicit `strip_alt_svc_h3` applies to every row):
| Client | Configuration | `h3` in `Alt-Svc` | Its HTTP/3 then |
|---|---|---|---|
| Explicit proxy client (`CONNECT`, absolute-form) | plain forward proxy | kept | goes around the proxy |
| Explicit proxy client | `--mitm` or `--transparent` (with or without `--transparent-quic-port`) | stripped | stays on TCP through the proxy; a client whose UDP is redirected to the QUIC port anyway may turn stripping off (`--keep-alt-svc-h3`) |
| Redirected client (SNI-routed TLS, HTTP routed by `Host`) | `--transparent`, no QUIC port | stripped | stays on TCP; block UDP 443 for DNS `HTTPS` records |
| Redirected client | `--transparent --transparent-quic-port` | kept | redirected to the QUIC port: intercepted (`h3` with `--mitm`) or relayed |
| HTTP/3 client of the transparent QUIC port | `--transparent-quic-port` | kept | already on HTTP/3 through the proxy |
| Reverse proxy client | `--http3` | the proxy's own `h3` | the proxy's HTTP/3 listener |
HTTP/2 can also announce alternatives in `ALTSVC` frames. The proxy never sends them, and drops
any that HTTP/2 servers send (as frames of a type it does not handle), so alternatives reach
HTTP/2 clients only as `Alt-Svc` headers, which are stripped the same way.
## Limitations and what is next
- **Transparent QUIC**'s original destination is the SNI's host (no TPROXY, no
`SO_ORIGINAL_DST`). Clients of an explicit proxy are still steered to TCP, unless they use the
forward proxy's HTTP/3 listener.
- **QUIC stream hooks.** Streams are paired as they open, so the hooks cannot open streams of
their own, nor see QUIC frames (only stream bytes and datagrams). The server's handshake comes
before the client's, so a server that is slow to answer delays the client's, which may
retransmit its Initials meanwhile; one that cannot be reached drops the flow rather than relay
it. 0-RTT is not offered, as the QUIC module has none.
- **QUIC message framing.** A connection's streams all get the framer its ALPN or host chooses,
each carrying that protocol's messages from its start (a stream type or a header before them,
as HTTP/3's unidirectional streams have, needs a framer of one's own). Framers keep no state
between calls, and a script's framer is called with all the bytes kept so far each time. Bytes
a framer cannot read reset the stream rather than fall back to the reads, and the hooks are not
told of the resets framing makes.
- **MASQUE.** QUIC inside a CONNECT-UDP flow is intercepted only with `--masque-intercept-quic`,
and a flow is inspected from its first datagram: one that starts with anything but a QUIC
Initial is relayed as UDP throughout, and once one of its QUIC connections is relayed, so is the
rest of the flow. An intercepted connection that its client carries over to another CONNECT-UDP
flow to the same target follows it there (a flow whose first datagram belongs to the connection
goes to the proxy's QUIC server, which validates the path through the new flow), and closes when
the flow it is on ends, not one it left; relayed QUIC that moves between flows is not followed.
CONNECT-UDP to an upstream MASQUE proxy (HTTP/3
chained proxies) and CONNECT-IP are not done.
- Server push (never requested: the proxy sends no MAX_PUSH_ID).
- The proxy's own QUIC connections to servers (stream by stream, HTTP/3 to origins) never move to
a new local address, having no reason to; they move to a server's preferred address as any
client does, and do not follow a server that moves to another address of its own accord (see
the QUIC module's [README](https://github.com/mahmoudimus/MicroProxy/blob/main/quic/README.md)).
- HTTP/3 to servers only over direct connections, and without client certificates.
# CONNECT-UDP (MASQUE)
Proxy UDP for MASQUE clients with CONNECT-UDP (RFC 9298) over HTTP/3, HTTP/2 and HTTP/1.1, see and
rewrite each datagram with UDP hooks, and optionally intercept the QUIC carried inside the flows.
Use this page to tunnel DNS, QUIC or any UDP through the proxy; the HTTP/3 listener itself is
described in [An HTTP/3 forward-proxy listener](https://mahmoudimus.github.io/MicroProxy/guides/http3/#an-http3-forward-proxy-listener).
## CONNECT-UDP over HTTP/3, HTTP/2 and HTTP/1.1
A forward proxy proxies UDP with CONNECT-UDP (RFC 9298) on its HTTP/3 listener (`--http3`) and,
with `--http3` or `--masque` (`withConnectUdp(true)`, `masque=true`), on its TCP listener too:
| Transport | Request | UDP payloads |
|---|---|---|
| HTTP/3 (`--http3`) | extended `CONNECT`, `:protocol connect-udp` (section 3.5) | QUIC DATAGRAM frames, or DATAGRAM capsules on the stream |
| HTTP/2: `h2` on the TLS listener (on by default), or `h2c` (`--http2-cleartext`) | extended `CONNECT`, `:protocol connect-udp` (section 3.4); the proxy sends `SETTINGS_ENABLE_CONNECT_PROTOCOL` | DATAGRAM capsules in the stream's DATA |
| HTTP/1.1, plain or on the TLS listener | `GET` with `Connection: Upgrade` and `Upgrade: connect-udp` (section 3.2), in origin-form or absolute-form; the answer is a `101` | DATAGRAM capsules on the connection, which closes with the flow |
`--masque` serves the TCP listener without an HTTP/3 listener (so no certificate or UDP port is
needed, and h2c works), and needs the `quic` and `http3-codec` jars as `--http3` does; a reverse
proxy fails to start with it. The request's path names the target by the URI template, by
default the well-known `/.well-known/masque/udp/{target_host}/{target_port}/`
(`--masque-udp-template`, `Http3Options.connectUdpTemplate`, `masque_udp_template`: a path with
an optional query, or an absolute URI, whose authority is the proxy's own, with each variable
once; values are percent-decoded, so an IPv6 address's colons may be encoded). Everything below
is the same whichever transport carries the flow.
- **Policy.** The request is an exchange like any other: authentication applies (`407`), filters
see it as the HTTP/1.1 upgrade an HTTP/1.1 client would send (`GET` of the path with `Upgrade:
connect-udp`, version `HTTP/3.0`, `HTTP/2.0` or `HTTP/1.1`) and may answer it, the target is
routed as `CONNECT host:port` would be (chained proxies carry no UDP, so a route through them
only gets `502`), `proxyToServerRequest` may answer, and the target's name is resolved by the
proxy's resolver and the filters' resolution hooks (`502` when it does not resolve). A path
that does not match the template gets `400`. The acceptance is a `101` with `Upgrade:
connect-udp` for filters and trackers, and a `200` (HTTP/2, HTTP/3) or that `101` (HTTP/1.1)
with `capsule-protocol: ?1` for the client. CONNECT-UDP elsewhere (a reverse proxy, an
intercepted session, a proxy without `--masque` or `--http3`) gets `501` over HTTP/2 and
HTTP/3; over HTTP/1.1 such a request is passed on as any upgrade.
- **HTTP Datagrams** (RFC 9297). UDP payloads travel as HTTP Datagrams with context ID 0: over
HTTP/3 in QUIC DATAGRAM frames, whose payload starts with the stream's quarter stream ID, or in
DATAGRAM capsules on the request stream (the capsule protocol, RFC 9297 section 3), which the
proxy reads whatever the client negotiated; HTTP/2 and HTTP/1.1 have no datagrams, so there
every payload is a capsule (section 3.5), both ways. Capsules of unknown types are skipped,
however large, and payloads with other context IDs dropped; a stream that ends inside a capsule,
or a DATAGRAM capsule larger than any UDP payload, resets the flow (HTTP/2 `CANCEL`). Towards
an HTTP/3 client the proxy uses DATAGRAM frames when the client's SETTINGS_H3_DATAGRAM and QUIC
transport parameters allow them and the payload fits, else capsules.
- **Relaying.** Each flow has a UDP socket of its own, connected to the target; datagrams go both
ways, one for one, until the client ends or resets its stream (half-closes its connection, for
HTTP/1.1), or the flow is idle for `--masque-udp-idle-timeout` (60 s), after which the proxy
ends the stream.
- **Bounds** (`Http3Options`). Flows at once, in total (4096) and per client connection (64;
`--masque-max-udp-flows`, `--masque-max-udp-flows-per-connection`; an HTTP/1.1 connection
carries one): a request past either gets `503`. Each direction of a flow queues at most 128
datagrams and 512 KiB (`connectUdpQueue`); past that datagrams are dropped, as a network drops
what it cannot carry, so a client that does not read its stream, or a slow target, holds up
nothing else.
- **Room after an end.** A flow holds its places in the bounds until the proxy has seen it end,
and no longer: the proxy gives them back before it tells the client of an end of its own (a
refusal, the idle timeout), and as soon as it reads the client's (a reset, the end of its
stream, the close of its connection), before it reads anything after it. A request the client
sends right after ending a flow on the same connection finds room for it: over HTTP/2 in the
order of the frames, over HTTP/3 in the order QUIC received them. Across connections there is
no such order, and a client that ends a flow on one and opens one on another may have the new
request read first: a request that finds the proxy's bound full waits, for at most
`--masque-release-wait` (250 ms), while flows whose end the proxy has seen are still closing,
and is refused at once when none are. A client that has read the proxy's end of a flow (its
reset or its own end of the stream) always finds that flow's places free.
- **Flow control.** A capsule to the client is stream data: over HTTP/2 it waits for the
stream's and the connection's send window (over HTTP/3 for QUIC's, over HTTP/1.1 for TCP's),
as any DATA does. While it waits, the target's later datagrams queue up to the bounds above
and those past them are dropped, counted in `masque.datagrams_dropped`: UDP is lossy, and stale
datagrams are worth less than the window they would take. A client that opens no window for the
idle timeout (`--idle-timeout`) has its flow reset, as a stalled tunnel would. The client's own
capsules are read as they come, crediting the windows back at once, whatever the target does.
- **Hooks.** `withUdpInterceptor(UdpInterceptor)` sees every datagram, both ways: `udpStart`,
`udpMessage` (return the payload to send, changed or not, or `null` to drop it), `udpEnd`, and
`UdpContext.send` to add datagrams; scripts get [`on_udp_message`](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#udp-hooks). A flow whose
QUIC the proxy intercepts ([below](https://mahmoudimus.github.io/MicroProxy/guides/masque/#quic-inside-connect-udp-flows)) shows the hooks no
datagrams: its QUIC ends at the proxy.
- **QUIC.** With `--masque-intercept-quic`, the QUIC inside a flow is decided on as transparent
QUIC is ([QUIC inside CONNECT-UDP flows](https://mahmoudimus.github.io/MicroProxy/guides/masque/#quic-inside-connect-udp-flows)); without it, it is
relayed like any UDP.
```java
bootstrap.withUdpInterceptor((msg, ctx) ->
msg.fromClient() && ctx.port() == 53 && msg.text().contains("blocked") ? null : msg.data());
```
A MASQUE client, such as aioquic's `H3Connection` (with HTTP/3 datagrams on), opens a flow and
sends a DNS query through it:
```python
http = H3Connection(quic, enable_webtransport=True) # sends SETTINGS_H3_DATAGRAM
http.send_headers(stream_id, [(b":method", b"CONNECT"), (b":protocol", b"connect-udp"),
(b":scheme", b"https"), (b":authority", b"proxy.example:8443"),
(b":path", b"/.well-known/masque/udp/192.0.2.53/53/"),
(b"capsule-protocol", b"?1")])
# once the 200 arrives: context ID 0, then the UDP payload
http.send_datagram(stream_id, b"\x00" + dns_query)
```
Over TCP the same query goes in a DATAGRAM capsule: its type (0), its length, then the context ID
(0) and the payload, as stream DATA on HTTP/2, or on the connection once an HTTP/1.1 client has
the `101`:
```python
# HTTP/1.1, to a proxy started with --masque (any HTTP/2 client sends the same capsule in DATA)
sock.sendall(b"GET /.well-known/masque/udp/192.0.2.53/53/ HTTP/1.1\r\nHost: proxy.example:8080\r\n"
b"Connection: Upgrade\r\nUpgrade: connect-udp\r\nCapsule-Protocol: ?1\r\n\r\n")
# once "HTTP/1.1 101 Switching Protocols" has arrived (a payload under 63 bytes keeps the length to one byte)
sock.sendall(bytes([0x00, 1 + len(dns_query), 0x00]) + dns_query)
```
## QUIC inside CONNECT-UDP flows
With `--masque-intercept-quic` (`Http3Options.Builder.connectUdpInterceptQuic(true)`,
`masque_intercept_quic=true`), the QUIC that a [MASQUE client](https://mahmoudimus.github.io/MicroProxy/guides/masque/#connect-udp-masque) sends through
a CONNECT-UDP flow is decided on as [transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic) is, with the flow's
target as the server, instead of being relayed as any UDP:
- **Which flows.** Those to a UDP port of `--masque-quic-ports` (`connectUdpQuicPorts`,
`masque_quic_ports`; 443 by default, `any` for every port) whose first datagram from the client
is a QUIC Initial. Other flows, and flows whose first datagram is something else, are relayed as
before, through the [UDP hooks](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#udp-hooks).
- **Deciding.** The proxy holds the flow's datagrams, reads the ClientHello from its Initials,
and runs the [same decision](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic): a `CONNECT target` is made up for it, the
[ignore and allow rules](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#ignoring-and-allowing-hosts) apply to the target and the SNI, then
the ALPN, `shouldIntercept` and `on_client_hello`. The MASQUE client's CONNECT-UDP request was
authenticated already, so the inner flow is not asked again, and its client connection has the
MASQUE client's address and user.
- **Intercepted.** HTTP/3 is served as HTTP/3, its requests going to the target (by the SNI's
name when the target is an IP address, so that the server's certificate is checked against it,
at the target's address): filters, scripts (`on_request`, `on_frame`, ...) and trackers see
them, with `FlowContext.isTransparentQuic()` true. Other QUIC goes through the
[QUIC stream hooks](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/), with `QuicContext.masque()` true. The proxy's
replies travel back in the flow, as HTTP Datagrams or capsules; over HTTP/2 and HTTP/1.1 (with
`--masque`, which needs no `--http3`), every packet of the inner QUIC is a capsule.
- **Relayed.** An ignored or declined host, or QUIC that is not intercepted, goes to the target
from the flow's own socket, the held datagrams first, as without the option.
- **How.** A virtual listener: the transparent listener's per-flow logic (`QuicFlows`) over a
QUIC server without a socket (`QuicServer.unbound`), fed the flows' datagrams under addresses
it makes up for each flow, and handing its replies back to the flows.
```java
MicroProxy.bootstrap()
.withSslContextSource(new KeyStoreSslContextSource(Path.of("proxy.p12"), password))
.withHttp3(true)
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca))
.withHttp3Options(Http3Options.builder()
.connectUdpInterceptQuic(true) // --masque-intercept-quic
.connectUdpQuicPorts(443, 8443) // --masque-quic-ports 443,8443
.build())
.withIgnoreHosts("\\.bank\\.example:443$") // relayed untouched
.start();
```
# QUIC stream hooks
Intercept QUIC that does not carry HTTP/3 (DNS over QUIC, `hq-interop`, protocols of your own) and
see, rewrite, drop and add its stream bytes, whole framed messages and datagrams. Use this page for
non-HTTP QUIC arriving through [transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic) or
[CONNECT-UDP flows](https://mahmoudimus.github.io/MicroProxy/guides/masque/#quic-inside-connect-udp-flows); scripts use the [`on_quic_*`
hooks](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#quic-hooks).
## Intercepting QUIC stream by stream
QUIC that does not carry HTTP/3 can be intercepted too, stream by stream:
`withQuicInterceptor(QuicInterceptor)` (or a script's [`on_quic_*`](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#quic-hooks) hooks) makes the
proxy intercept the [transparent QUIC](https://mahmoudimus.github.io/MicroProxy/guides/transparent-mode/#transparent-quic) flows, and the QUIC inside
[CONNECT-UDP flows](https://mahmoudimus.github.io/MicroProxy/guides/masque/#quic-inside-connect-udp-flows) with `--masque-intercept-quic`, whose ALPN
offers no `h3` (`doq`, `hq-interop`, a protocol of its own), once the other rules allow it, as the
[TCP hooks](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/#raw-tcp-streams) do for TLS that carries no HTTP. `withQuicHosts(...)` (`--quic-hosts`,
`quic_hosts`) names hosts, by regular expressions searched for in the server's host name and the
SNI, whose QUIC is relayed this way whatever its ALPN: their HTTP/3 too, which filters and frame
hooks then do not see, and their other QUIC even without an interceptor.
```java
MicroProxy.bootstrap()
.withTransparent(true)
.withTransparentQuicPort(8443)
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca))
.withQuicHosts("^quic\\.example\\.com$") // --quic-hosts, quic_hosts
.withQuicInterceptor(new QuicInterceptor() {
@Override
public void quicStart(QuicContext ctx) {
log("QUIC " + ctx.alpn() + " to " + ctx.host() + ":" + ctx.port() + " (" + ctx.sni() + ")");
}
@Override
public byte[] streamData(QuicStreamMessage msg, QuicStreamContext stream) {
return stream.clientInitiated() && !msg.fromClient()
? msg.text().replace("secret", "******").getBytes(UTF_8) : msg.data();
}
@Override
public byte[] datagram(QuicDatagram datagram, QuicContext ctx) {
return datagram.length() > 1000 ? null : datagram.data(); // drop large ones
}
})
.start();
```
- **Pairing the connections.** The proxy first connects to the server, from a UDP socket of the
flow's own, with the client's SNI and ALPN protocols, validating the server as it validates
[HTTP/3 servers](https://mahmoudimus.github.io/MicroProxy/guides/http3/#http3-to-servers) (the MITM manager's server trust). Then it completes the
client's handshake with a certificate its CA issues for the SNI, choosing the protocol the
server chose, and taking DATAGRAM frames only when the server does. A server that cannot be
reached drops the flow, as a relay that cannot be made does.
- **Streams.** Each stream one side opens, bidirectional or unidirectional, is opened on the
other connection when the peers' stream limits allow it, in order, so the stream IDs on both
connections are usually the same (`QuicStreamContext.streamId()` is the client's,
`serverStreamId()` the server's). The hooks: `quicStart`; `streamOpened`; `streamData` for the
bytes of each read, which returns the bytes to pass on (`msg.data()` as it is or changed in
place, others, or `null` or an empty array to drop them); `streamFinished` before a FIN is passed
on; `streamReset` (RESET_STREAM, passed on with its code) and `streamStopped` (STOP_SENDING:
the other side is asked the same, and the stream reset towards the side that asked); `datagram`
for each datagram, which returns the payload or `null`; then `quicEnd`, or `quicError` when a
connection timed out, was reset or failed with a transport error, never both. When one side
closes its connection, the other is closed with the same application error code and reason.
- **Adding data.** `QuicStreamContext.send(fromClient, bytes)` writes bytes to a stream as if that
side had sent them, after the message being handled when called from a `streamData` of the same
direction; `QuicContext.sendDatagram(fromClient, bytes)` sends a datagram. Neither passes
through the hooks.
- **Messages.** A stream is a byte stream, as TCP is: a `QuicStreamMessage` is whatever one read
returned (at most 16 KiB), so a peer's write may show up in pieces, or joined with the next one.
Hooks that look for a protocol's messages keep the start of one until its end arrives, or let
the proxy cut the stream into messages for `streamMessage`
([QUIC message framing](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/#quic-message-framing)).
- **Flow control.** End to end: each direction of each stream is read only as fast as the other
side takes the bytes, so QUIC's flow control holds a fast sender back while a slow receiver
reads, and the proxy keeps at most one read per direction. The proxy's receive windows are
HTTP/3's (`Http3Options.streamReceiveWindow`, `connectionReceiveWindow`).
- **Threads.** Each direction of each stream, and of datagrams, has a virtual thread of its own:
the interceptor must be thread-safe; within one direction of one stream, calls come in order.
The context names the client connection (`connectionId`, `clientAddress`, `flowContext`), the
server (`host`, `port`, `serverAddress`), `sni`, `alpn` and `masque()`.
## QUIC message framing
A stream's bytes come to `streamData` in reads, without the protocol's message boundaries. Many
protocols over QUIC frame their messages simply, and the proxy can cut their streams into whole
messages for a hook: `QuicInterceptor.streamMessage(QuicMessage msg, QuicStreamContext stream)`
(or a script's [`on_quic_message`](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#quic-hooks)) sees each message's payload, without its length
prefix or delimiter, and returns the messages to send in its place. What it returns is framed
again, so an edited payload gets a length prefix of its new length.
`--quic-framing` (`quic_framing`, `withQuicFraming(QuicFraming)`) chooses a framer by ALPN or by
host: comma-separated `key=framer` rules, where a key is an ALPN protocol (`doq`), a host name
matched against the server's host and the SNI (`dns.example.com`, or `*.example.com` for it and
the names below it), or `*` for any connection. A host rule wins over an ALPN rule, which wins
over `*`. DNS over QUIC (`doq`) is framed with `u16` by default; `doq=none` turns that off.
| Framer | Messages |
|---|---|
| `u16` | a two-byte big-endian length, then the payload: DNS over QUIC (RFC 9250), DNS over TCP |
| `u32` | a four-byte big-endian length, then the payload |
| `varint` | a QUIC variable-length integer (RFC 9000 section 16) as the length; written in its shortest form |
| `lf` | a line ending with a line feed, which the payload leaves out (a carriage return before it stays) |
| `crlf` | a line ending with a carriage return and a line feed |
| `none` | not framed: `streamData` sees the reads |
| `script` | a script's `quic_message_length(data)` (see [QUIC](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#quic-hooks)) |
| a `MessageFramer` | from Java: `framing.withRule("myproto", framer)` or `withFramer(name, framer)` |
```bash
java -jar microproxy-starlark/target/microproxy-starlark-${MICROPROXY_VERSION}-all.jar --port 8080 \
--transparent --transparent-quic-port 8443 --mitm --script doq.star \
--quic-framing doq=u16,myproto=varint,*=none --quic-max-message-size 65537
```
- **Which streams.** Every stream of a connection whose framing rule names a framer, when the
interceptor overrides `streamMessage` (a script, when it defines `on_quic_message`). Framing
replaces the chunk hook there: `streamData` (`on_quic_stream_data`) is not called on those
streams, and is called as before on the streams of other connections. An interceptor that does
not override `streamMessage` is never framed, whatever the rules say: **without framing, or
without a message hook, nothing is kept**, and each read goes on as it came.
- **What a hook returns.** `List.of(msg)` passes the message on (changed in place or not); other
messages replace it, in order; `null` or an empty list drops it. A message whose `fromClient()`
is the other direction's (`msg.withFromClient(false)`) goes to the side that sent `msg`, on the
same stream, as if the other side had answered. `QuicStreamContext.send` still writes bytes as
they are, not framed. A hook that throws, or returns a payload its framer cannot frame (more
than 65535 bytes for `u16`), has the message passed on unchanged.
- **Limits.** A message larger than `--quic-max-message-size` (`quic_max_message_size`,
`QuicFraming.withMaxMessageSize`; 1 MiB, the prefix or delimiter included), known from a length
prefix as soon as it arrives or from a delimiter that does not come in time, and bytes a framer
cannot read (a custom framer that throws), **reset the stream** in both directions on both
connections, and ask both sides to stop sending, with `--quic-message-error-code`
(`quic_message_error_code`, `withErrorCode`; 2, DNS over QUIC's `DOQ_PROTOCOL_ERROR`), as a
WebSocket message too big closes its connection with 1009. The connection and its other
streams go on. Bytes left over when a side finishes its stream, not a whole message, are passed
on as they are, before the FIN.
- **Flow control** stays end to end: a direction keeps at most one message's bytes and a read.
- **Datagrams** are whole messages already: `datagram` (`on_quic_datagram`) sees them as before.
- **Decoding.** `QuicMessage` has `data()`, `text()`, `fromClient()`, `streamId()`, `alpn()`,
`timestamp()`, the copies `withData`, `withText`, `withFromClient` and `withDns`, and decoders:
`dns()` reads the payload as a DNS message (`org.microproxy.dns.DnsMessage`: the header, the
questions, and records with typed data for A, AAAA, NS, CNAME, PTR, DNAME, MX, SRV, TXT and
SOA; it encodes back, names compressed); `protobuf()` and `protobuf(schema, type)` decode
protobuf as the [content views](https://mahmoudimus.github.io/MicroProxy/guides/content-views/#content-views) do; `render()` renders the payload with the
best content view, as `dig` shows it for DNS over QUIC (the `dns` view), for logging.
DNS over QUIC, logging each query and answering a blocked name with `0.0.0.0` by rewriting the
server's answer:
```java
Set blocked = Set.of("ads.example", "tracker.example");
bootstrap.withQuicInterceptor(new QuicInterceptor() {
@Override
public byte[] streamData(QuicStreamMessage msg, QuicStreamContext stream) {
return msg.data(); // streams that are not framed
}
@Override
public List streamMessage(QuicMessage msg, QuicStreamContext stream) {
DnsMessage dns = msg.dns();
DnsMessage.Question q = dns.questions().getFirst();
if (msg.fromClient()) {
log(stream.clientAddress() + " asks for " + q.name() + " " + q.typeName());
return List.of(msg);
}
if (!blocked.contains(q.name().toLowerCase(Locale.ROOT))) return List.of(msg);
List answers = dns.answers().stream()
.map(r -> r.type() == DnsMessage.A ? r.withData(DnsMessage.Address.of("0.0.0.0")) : r)
.toList();
return List.of(msg.withDns(dns.withAnswers(answers))); // framed again, its length recomputed
}
});
```
# Frame interception
See, edit, drop and add HTTP/2 and HTTP/3 frames on live connections, on both sides of the proxy,
while the proxy keeps every connection protocol-correct. Use this page to write a Java
`FrameInterceptor`; scripts get the same with [`on_frame`](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#frames).
## HTTP/2 frame interception
A `FrameInterceptor` (`org.microproxy.frames`) sees every HTTP/2 frame on live connections and may
edit, drop and add frames. It is set once per proxy, not per exchange, because SETTINGS, PING and
GOAWAY belong to a connection:
```java
MicroProxy.bootstrap()
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca)) // HTTP/2 both ways by default
.withFrameInterceptor((frame, direction, ctx) -> switch (frame) {
case Http2Frame.Headers h when direction == FrameDirection.TO_SERVER -> h.withHeader("x-via-h2", "1");
case Http2Frame.Data d when direction == FrameDirection.FROM_SERVER && d.text().contains("secret") ->
d.withText(d.text().replace("secret", "******")); // same length: content-length still holds
case Http2Frame.Priority p -> null; // drop
default -> frame; // pass through
})
.start();
```
- **Frames.** `Http2Frame` is a sealed interface of records: `Data`, `Headers` (the decoded field
list, pseudo-headers included, with CONTINUATION folded in), `Priority`, `RstStream`, `Settings`,
`PushPromise`, `Ping`, `GoAway`, `WindowUpdate` and `Unknown` (extension frames, which the proxy
otherwise discards unread). `Http3Frame` has `Data`, `Headers`, `Settings`, `GoAway`,
`CancelPush`, `MaxPushId`, `PushPromise` and `Unknown`. Edit them with their `with...` methods.
- **Directions.** The proxy terminates HTTP/2 on each side, so each hop has its own frames:
`FROM_CLIENT` (after decoding, before the proxy acts on them), `TO_CLIENT` (before encoding),
`FROM_SERVER` and `TO_SERVER`. A request seen `FROM_CLIENT` is a different set of frames from the
one later seen `TO_SERVER`: other stream ids, header blocks and DATA boundaries.
- **Context.** `FrameContext` gives the protocol, the client connection's id, the client's address,
the server (`host:port`, on the server side), the stream id, the exchange's `FlowContext` when
the frame belongs to one, and `send(frame)`, which adds a frame after this one on the same hop
and in the same direction (going out it is sent next; coming in the proxy processes it as if the
peer had sent it).
- **Coverage.** Clients on intercepted TLS, `h2c` and the TLS listener; connections to servers
with all their multiplexed streams; full-duplex gRPC; `CONNECT` tunnels and WebSockets over
HTTP/2, whose bytes are DATA frames. DATA the proxy sends is shown in pieces of at most 16 KiB.
And live HTTP/3, on both sides, under the same rules adapted to HTTP/3 (see
[HTTP/3 frames](https://mahmoudimus.github.io/MicroProxy/guides/frame-interception/#http3-frame-interception)).
- **Cost.** Without an interceptor nothing changes: no conversion, no allocation, no call.
The proxy stays protocol-correct whatever the interceptor returns. What it accepts:
| Frame | May be edited | May be dropped | May be followed by (`ctx.send`) |
|---|---|---|---|
| DATA | the payload, to any size | yes | DATA; HEADERS (trailers), last, when the frame ends the stream; extension frames |
| HEADERS | the fields | no | HEADERS; extension frames |
| PRIORITY | everything but the stream | yes | extension frames |
| RST_STREAM | the error code | no | extension frames |
| SETTINGS | received: values, only to make the proxy stricter; sent: only extension settings | no | extension frames |
| GOAWAY | the error code and debug data | no | extension frames |
| extension (`Unknown`) | everything | yes | extension frames |
| SETTINGS ACK, PING, WINDOW_UPDATE | nothing (the proxy's own flow control and liveness) | no | nothing |
- **Stream ids and END_STREAM are the proxy's.** Frames stay on their stream (extension frames may
also use stream 0). When the frame ended its stream, the last DATA or HEADERS frame of the result
does; a dropped DATA frame that ended its stream is replaced by an empty DATA frame with
END_STREAM. What the interceptor sets is ignored.
- **Header fields** must be valid (`Field.validate`: lower-case token names, known pseudo-headers
first, no connection-specific fields, no CR, LF or NUL in values). HPACK is never at risk: blocks
are decoded once on arrival and encoded once on sending. A malformed request or response the
proxy receives resets its stream, as usual. Changing a body's length while its HEADERS declare
`content-length`: fix or remove the field in the HEADERS too (the interceptor sees them first),
or the proxy resets the stream as malformed.
- **SETTINGS received** may lower `HEADER_TABLE_SIZE`, `INITIAL_WINDOW_SIZE`, `MAX_FRAME_SIZE` (not
below 16384), `MAX_CONCURRENT_STREAMS` and `MAX_HEADER_LIST_SIZE`, or clear `ENABLE_PUSH` and
`ENABLE_CONNECT_PROTOCOL`; the proxy then uses the edited values (fewer streams to a server, say).
- **Flow control.** Received DATA counts against the windows, and is credited back, at its size on
the wire, whatever the interceptor makes of it. DATA the proxy sends waits for window at its size
after interception. Rate limits count frames as they arrive.
- **Illegal results and failures.** A result that breaks a rule is rejected as a whole: it is
logged (logger `org.microproxy.impl.Http2Frames`, `WARNING`) and the original frame goes on, as
it does when the interceptor throws. `ctx.send` after the call has returned throws
`IllegalStateException`.
- **Threads.** The interceptor is called from many threads at once and must be thread-safe. The
HEADERS that open a stream to a server are intercepted while that connection's writes are held,
so stream ids stay in order: keep that call short.
Scripts get the same with [`on_frame`](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#frames).
## HTTP/3 frame interception
The [frame interceptor](https://mahmoudimus.github.io/MicroProxy/guides/frame-interception/#frame-interception) sees HTTP/3 frames live wherever the proxy speaks
HTTP/3: `FROM_CLIENT` and `TO_CLIENT` on the reverse proxy's HTTP/3 listener, on intercepted
transparent QUIC and on HTTP/3 intercepted inside CONNECT-UDP flows, `TO_SERVER` and `FROM_SERVER` on connections to HTTP/3 servers, with the
stream's QUIC id and, from a stream's
first frame on, its exchange's `FlowContext`. `Http3Frame` has `Data`, `Headers`, `Settings`,
`GoAway`, `CancelPush`, `MaxPushId`, `PushPromise` and `Unknown`. The rules are HTTP/2's,
adapted: HTTP/3 frames carry no stream id or END_STREAM (a message ends with its stream), so
results must keep the message's order (HEADERS, DATA, trailers; interim responses first); DATA
may be edited to any size, and QUIC's flow control counts what is actually sent; field sections
are decoded through QPACK before the interceptor sees them and encoded through the connection's
QPACK encoder after it, so QPACK state never depends on it; SETTINGS received may only make the
proxy stricter (a smaller QPACK table, field section or blocked-streams limit, no
ENABLE_CONNECT_PROTOCOL or H3_DATAGRAM turned on), SETTINGS sent may change only in extension
settings; GOAWAY, MAX_PUSH_ID and CANCEL_PUSH are read-only; extension and grease frames may be
edited, dropped and added. Scripts get the same with [`on_frame`](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#frames).
For HTTP/3 traffic relayed by other means, `Http3FramePipeline` runs a
frame interceptor over a request stream's or a control stream's bytes, under the same rules, and
writes them back with static-only QPACK, so no encoder stream is needed. It needs only the
`http3-codec` jar (`Http3FramePipeline.available()`).
```java
Http3FramePipeline pipeline = Http3FramePipeline.builder(interceptor)
.direction(FrameDirection.FROM_CLIENT) // whose frames these are
.build();
pipeline.controlStream(2, clientControlStream, out); // stream type, SETTINGS, ...
pipeline.encoderStream(clientQpackEncoderStream); // only when the client's QPACK uses its dynamic table
pipeline.requestStream(0, requestStream, rewritten); // HEADERS, DATA, trailers
```
# WebSockets
Watch, rewrite, drop and add WebSocket frames and whole messages in both directions, over HTTP/1
upgrades and HTTP/2 and HTTP/3 extended `CONNECT`, inside intercepted TLS and through
permessage-deflate. Use this page to write WebSocket filters in Java; scripts use
[`on_websocket_frame` and `on_websocket_message`](https://mahmoudimus.github.io/MicroProxy/guides/scripting/#websocket-examples).
## WebSocket frames and messages
After a `101` upgrade or HTTP/2 WebSocket extended CONNECT, filters can watch and rewrite every
frame, or every whole message, in both directions and inside intercepted TLS:
- **Watching:** `webSocketFrameReceived(WebSocketFrame frame, boolean fromClient)` sees the
opcode, FIN, masking and the unmasked payload. LittleProxy's
`webSocketFrameReceived(Supplier, boolean)` still works and receives each frame's raw
bytes (LittleProxy delivered raw TCP reads rather than frames).
- **Rewriting frames:** `filterWebSocketFrame(WebSocketFrame frame, boolean fromClient)` returns
the frame to forward: `frame` itself, a replacement (`frame.withText(...)`,
`WebSocketFrame.text(...)`, `binary(...)`, `close(code, reason)`), or `null` to drop it. The
proxy masks frames it sends towards the server, as RFC 6455 requires. Messages split into
continuation frames are seen one frame at a time.
- **Rewriting messages:** `filterWebSocketMessage(WebSocketMessage message, boolean fromClient)`
sees a whole text or binary message, its frames joined. `WebSocketMessage` is a record (`type`
`TEXT` or `BINARY`, `payload`, `fromClient`) with `text()`, `withText(...)`,
`withPayload(...)`, `withFromClient(...)` and the factories `WebSocketMessage.text(...)` and
`binary(...)`. Return the message, a replacement, or `null` to drop it. Override
`filterWebSocketMessages` instead to return a list: each message goes towards the server when
its `fromClient()` is true and towards the client otherwise, so a filter can answer a message
itself or add messages in either direction. `HttpFilters.builder().onWebSocketMessage(...)`
takes a lambda.
- **Order:** with both hooks, each frame goes through `filterWebSocketFrame` first, and
`filterWebSocketMessage` sees the message joined from what it returned (a dropped frame adds
nothing). Control frames (ping, pong, close) that arrive between a message's frames reach the
frame hook and are forwarded at once; they never wait for the message.
- **Fragmentation:** a message returned unchanged goes out split as it arrived; an edited one
goes out as a single frame.
- **Limits:** frames larger than `withMaxWebSocketFrameBufferSize` (default 1 MiB) arrive at the
frame hook truncated, without a payload: returning them streams them through, `null` discards
them. Frames of a message being joined, and compressed frames, are buffered whole instead, up
to `withMaxWebSocketMessageSize` (default 16 MiB). A message larger than that, joined or
inflated (a decompression bomb), fails the connection: both peers get a close frame with status
1009 (message too big), as mitmproxy does. Frames out of order (a continuation outside a
message, a new message before the last one ended) and invalid compressed data fail it with
1002.
- **No listener:** if no filter overrides any of these methods, the connection is relayed as raw
bytes. Frames are only joined into messages for filters that override a message hook, so frame
filters keep relaying frame by frame, and filters that only watch frames leave compressed
frames as they are (RSV1 set).
```java
@Override
public WebSocketFrame filterWebSocketFrame(WebSocketFrame frame, boolean fromClient) {
if (!frame.isText()) return frame;
String text = frame.payloadAsText();
if (fromClient && text.contains("\"password\"")) return null; // drop it
return frame.withText(text.replace("staging.example", "prod.example")); // or rewrite it
}
@Override
public WebSocketMessage filterWebSocketMessage(WebSocketMessage message, boolean fromClient) {
if (message.isBinary() && message.length() > 1 << 20) return null; // drop big uploads
return message.isText() ? message.withText(message.text().strip()) : message;
}
```
### permessage-deflate (RFC 7692)
Filters that rewrite frames or messages read compressed
traffic as plain text: the proxy inflates what each peer sends and compresses what it forwards
again. Each direction has two compression contexts, one per hop (client to proxy, proxy to
server), because a hook may change payloads, so the proxy cannot pass compressed bytes on and keep
the receiver's history in step with the sender's. The proxy keeps only the permessage-deflate
offers in the upgrade request's `Sec-WebSocket-Extensions` (it could not read frames of other
extensions) and otherwise leaves the negotiation to the client and server; it then applies what
the server's answer says to the server's hop and what the client received to the client's hop.
- `client_no_context_takeover` and `server_no_context_takeover` are honoured per direction: the
proxy resets its compressor after each message when it is the sender that agreed to it, and its
decompressor when the peer is.
- `server_max_window_bits` and `client_max_window_bits`: the JDK's `Inflater` always uses a 32
KiB window, which reads anything a peer sends whatever window it agreed to. The JDK's `Deflater`
cannot use a smaller window, and a peer that agreed to a smaller one may keep only that much
history, so a back-reference further back would corrupt what it reads. Resetting the compressor
after each message would not help, since one message longer than the window can refer back
across it. Towards a peer whose window is under 15 bits the proxy therefore compresses with
Huffman coding only (`Deflater.HUFFMAN_ONLY`): no back-references at all, which any window
decodes. The parameters stay as the peers agreed them; only the proxy's own output compresses
less.
- A message goes out compressed when its sender compressed it and the hop agreed to compression;
a message a filter sends back to a peer is compressed whenever that hop agreed to it.
- `java.net.http.WebSocket` never offers extensions, so the tests use peers built on `Deflater`
and `Inflater`.
# Raw TCP and TLS hooks
See, rewrite, drop and add the bytes of raw TCP tunnels, including TLS sessions the proxy decrypts
that do not carry HTTP (SMTPS, IMAPS, MQTT over TLS), with mitmproxy's TCP hooks. Use this page for
non-HTTP protocols; UDP flows and QUIC streams have hooks of their own ([CONNECT-UDP](https://mahmoudimus.github.io/MicroProxy/guides/masque/),
[QUIC stream hooks](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/)).
## Raw TCP streams
`withTcpInterceptor(TcpInterceptor)` shows the bytes of every raw TCP tunnel to one interceptor,
with mitmproxy's TCP hooks: `tcpStart(ctx)`; `tcpMessage(msg, ctx)` for the bytes of each read,
which returns the bytes to pass on (`msg.data()` as it is or changed in place, others, or `null`
or an empty array to drop them); `tcpHalfClose(ctx, fromClient)` when one side finishes sending,
before the end is passed on; then `tcpEnd(ctx)`, or `tcpError(ctx, error)` when a read or write
failed, never both. `ctx.send(fromClient, bytes)` adds bytes as if that side had sent them, at any
time while that direction is open; sent from a `tcpMessage` call in the same direction, they
follow the message. `TcpContext` also gives the client connection's id and address, the target's
host and port, whether the proxy decrypted the client's TLS (`tlsIntercepted()`), the SNI and the
negotiated ALPN protocol.
```java
MicroProxy.bootstrap()
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca))
.withTcpHosts("^mail\\.example\\.com$") // --tcp-hosts, tcp_hosts
.withTcpInterceptor((msg, ctx) -> {
if (!msg.fromClient() && msg.text().startsWith("220 ")) {
return msg.text().replace("ESMTP", "ESMTP (via MicroProxy)").getBytes(UTF_8);
}
return msg.data();
})
.start();
```
- **Which tunnels.** A `CONNECT` that is not intercepted (no MITM manager, an ignored host, a
declined ClientHello, a protocol that is neither TLS nor HTTP, a server that speaks first), on
an HTTP/1 connection or as a stream of an HTTP/2 one; whether the server is reached directly,
through a chained proxy, on a stream of an HTTP/2 connection, or through the HTTP or HTTPS proxy
of a session ([In front of an HTTP or HTTPS proxy](https://mahmoudimus.github.io/MicroProxy/guides/chained-proxies/#in-front-of-an-http-or-https-proxy));
`reverse:tcp://` connections; and a [SOCKS client](https://mahmoudimus.github.io/MicroProxy/guides/socks/#socks5-mode)'s connection that is not
intercepted or served as HTTP, which goes the way of a `CONNECT`. Upgraded WebSockets are not
tunnels: their frames and messages go to the
[WebSocket hooks](https://mahmoudimus.github.io/MicroProxy/guides/websockets/#websocket-frames-and-messages).
- **Messages.** TCP has no message boundaries. A `TcpMessage` is whatever one read returned (up
to a buffer, 16 KiB by default), with `fromClient()` and a timestamp; a peer's write may show
up in pieces, or joined with the next one. Hooks that look for lines or commands keep the start
of one until its end arrives.
- **Threads.** Each direction is read by a thread of its own, so a tunnel's hooks are called
from two threads at once: the interceptor must be thread-safe. Within a direction calls come in
order, and the direction waits for each.
- **Half-close.** The end of one direction is reported, then passed on; the other direction goes
on, as without hooks. (Intercepted TLS closes the whole connection there instead, as before:
TLS peers differ in their support for half-close.)
- **The fast path.** Without an interceptor, tunnels copy bytes through pooled buffers without
copying them again, as before; with one, each message gets an array of its own.
## TLS that does not carry HTTP
Once the client's TLS is intercepted, the proxy decides what
the session is, as mitmproxy's next layer does inside TLS: an `h2` client is served as HTTP/2;
the [TCP hosts](https://mahmoudimus.github.io/MicroProxy/guides/tcp-hooks/#raw-tcp-streams) are raw TCP; an ALPN protocol of HTTP/1 is HTTP and any other
(`imap`, `mqtt`, ...) raw TCP; without ALPN, a server that speaks first (an SMTP banner) makes the
session raw at once, while the proxy waits for the client's first bytes, and otherwise those bytes
decide: a request line is HTTP, anything else raw TCP. A raw session is relayed between the
decrypted streams through the TCP hooks, whose context says `tlsIntercepted()`, with the SNI and
ALPN. With an interceptor, a client whose ALPN offers no HTTP protocol (an MQTT client offering
`mqtt`, say) is intercepted rather than tunnelled: the server is offered the client's protocols
and the client gets the server's choice. `acme-tls/1` (the ACME TLS-ALPN challenge) is never
intercepted, since no certificate the proxy makes could answer it. Without an interceptor such
clients are tunnelled untouched, as before.
## TCP hosts
`withTcpHosts(...)` (`--tcp-hosts`, `tcp_hosts`) is mitmproxy's `tcp_hosts`: each
pattern is a regular expression searched for, ignoring case, in the target's host name and the
client's SNI (without ports). Their connections are raw TCP even when they speak HTTP: plain
bytes after a `CONNECT` are tunnelled rather than served as HTTP, and an intercepted TLS session
is relayed decrypted. TLS is still intercepted, unlike with `--ignore-hosts`.
## Limitations
A session can be relayed as raw TCP only over a server connection of its own:
one shared as an HTTP/2 connection, a pooled one or none at all (offline interception) leaves it
served as HTTP. Bytes added with `send` do not pass through `tcpMessage`, unlike mitmproxy's
injected messages. UDP flows ([CONNECT-UDP](https://mahmoudimus.github.io/MicroProxy/guides/masque/)) have hooks of their own,
`withUdpInterceptor`, and so do the streams of intercepted QUIC, `withQuicInterceptor`
([QUIC stream hooks](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/)).
# Rewriting bodies
Decode, edit and re-encode message bodies safely (compression, charsets, lengths), buffer one
message at a time from a filter, and rewrite headers and text bodies declaratively with
`RewriteRules`. Use this page before writing a filter that changes bodies.
`org.microproxy.http.HttpBodies` handles the parts of body editing that are easy to get wrong:
- **Decoding:** gzip, x-gzip, deflate (zlib-wrapped or raw), `br` (Brotli, through a vendored
copy of the pure-Java codec [jaybrotli](https://github.com/mahmoudimus/jaybrotli)) and, when
`zstd-decoder` is on the class path, `zstd`. Decoding is capped at 64 MiB by default to defuse
compression bombs; the Brotli decoder stops before writing past the cap. A `br` body must be
exactly one Brotli stream: as in the reference decoder, a command that runs past the end of its
meta-block, or any byte after the end of the stream, is an `IOException` ("corrupt brotli
data"), so the proxy and the client never disagree about where a body ends. `canDecode` reports
codings that can't be decoded, such as the dictionary codings `dcb` and `dcz`.
- **Negotiation:** `restrictAcceptEncoding(request)` trims a request's `Accept-Encoding` to the
codings above (keeping q-values), so the server never picks one the proxy can't read.
`RewriteRules` and scripts that define `on_response` do this automatically.
- **Charset:** taken from the *response's* `Content-Type`, defaulting to UTF-8.
- **Rewriting:** the original coding is re-applied, Brotli included, except that zstd is
re-encoded as gzip because there is no pure-Java encoder for it. Brotli is written at quality 5
(`HttpBodies.BROTLI_QUALITY`) with jaybrotli's default strategies, in the smallest window that
spans the body (see [Brotli speed and size](https://mahmoudimus.github.io/MicroProxy/guides/rewriting-bodies/#brotli-speed-and-size)). `ETag`/`Content-MD5` are
dropped because they no longer match. `Content-Length` is fixed when the message is written.
Filters can also ask to buffer one message at a time, after seeing its head:
- `HttpFilters.responseBufferSizeInBytes(response)`: a body larger than requested simply streams
through.
- `HttpFilters.requestBufferSizeInBytes(request)`: a body larger than requested is answered with
`413`, since part of it has already been read.
`org.microproxy.extras.RewriteRules` builds on both. It is a filters source with an ordered list
of rules. Each rule matches a URL regex (absolute form, `https://` inside intercepted sessions)
and edits request headers, response headers and textual response bodies:
```java
bootstrap.withFiltersSource(RewriteRules.builder()
.add(RewriteRules.Rule.matching("https://example\\.com/.*")
.replaceInBody("Example Domain", "Rewritten Domain")
.removeResponseHeader("Content-Security-Policy"))
.build());
```
Only matching text responses are buffered; everything else streams.
## Brotli speed and size
`br` is decoded and encoded by jaybrotli, a pure-Java codec vendored in the core. Its decoder runs
at a median 1.1 times the speed of Google's Java decoder, which MicroProxy used before (0.9 to 1.2
times in the table below). Rewritten bodies use quality 5 with jaybrotli's default strategies: the
setting that stays at about 10 MB/s or more on large HTML and JSON while coming out smaller than
gzip's default level on all but one of the bodies below. Qualities 4 and 6 are within 1% of it in
size, at the same speed. jaybrotli's hash-chain finder with lazy parsing is 4 to 5% smaller on
bodies of 500 KB to 1 MB but drops to about 6 MB/s on 1 MB of HTML; with greedy parsing (the setting
before jaybrotli's default strategies became fast) it is faster on small bodies but up to 20% larger
there, and about as fast on large ones.
Measured through `HttpBodies.decode` (the build before and after the switch, each in its own class
loader) and `HttpBodies.encode` (against `GZIPOutputStream`), in one JVM, interleaved round by
round: the median MB/s of 9 rounds of 0.4 s (decoding, MB/s of
output) or 7 rounds of 1 s (encoding, MB/s of input), with the minimum and maximum in brackets.
Host: a shared 4-vCPU Xeon at 2.1 GHz, OpenJDK 21, `nice -n 19`; compare within a row. The
bodies are javadoc and documentation pages (HTML), ISO code tables and an npm lock file (JSON);
the decoded streams come from the reference `brotli` encoder.
| Decoding | Google's decoder | jaybrotli |
|---|---:|---:|
| HTML 11 KB, q5 / q11 | 153 (110–172) / 156 (138–168) | 175 (145–194) / 172 (154–189) |
| HTML 170 KB, q5 / q11 | 435 (368–497) / 379 (119–394) | 431 (393–504) / 425 (363–480) |
| HTML 293 KB, q5 / q11 | 155 (141–182) / 154 (128–165) | 185 (144–206) / 157 (137–193) |
| HTML 1 MB, q5 / q11 | 153 (139–166) / 172 (131–175) | 172 (155–186) / 177 (155–204) |
| JSON 17 KB, q5 / q11 | 240 (197–264) / 176 (171–192) | 273 (231–295) / 215 (160–226) |
| JSON 498 KB, q5 / q11 | 340 (279–397) / 322 (247–360) | 319 (255–368) / 294 (228–344) |
| JSON 558 KB, q5 / q11 | 318 (265–341) / 255 (227–269) | 359 (287–376) / 289 (272–308) |
| Encoding | gzip -6 size | `br` size | gzip -6 MB/s | `br` MB/s |
|---|---:|---:|---:|---:|
| HTML 11 KB | 3,280 | 2,842 (−13%) | 51 (35–54) | 12 (11–12) |
| HTML 170 KB | 14,343 | 13,093 (−9%) | 87 (75–92) | 35 (33–40) |
| HTML 293 KB | 79,369 | 77,515 (−2%) | 23 (21–25) | 9.5 (8.8–10.8) |
| HTML 1 MB | 264,641 | 260,475 (−2%) | 29 (27–30) | 11 (10–11) |
| JSON 17 KB | 2,956 | 2,918 (−1%) | 120 (100–136) | 24 (23–27) |
| JSON 498 KB | 60,190 | 61,098 (+2%) | 76 (64–79) | 22 (22–25) |
| JSON 558 KB | 102,506 | 92,590 (−10%) | 63 (59–67) | 18 (17–20) |
gzip stays the faster encoder, by 2.4 to 5 times; Brotli is kept because the client asked for it,
and at this setting encoding costs at most about a millisecond per 10 KB of body.
# Addons, HAR and replay
Ready-made filters ported from mitmproxy (map local and remote, modify bodies and headers, block
lists, anti-cache, sticky cookies) with mitmproxy's option syntax and flow filter expressions, plus
HAR recording and server and client replay. Use this page to change, record or replay traffic from
the command line without writing code.
## mitmproxy-style addons
`org.microproxy.extras` has ports of mitmproxy's request and response addons. Each is an
`HttpFiltersSource`, so they compose with each other, with your own filters and with scripts
through `HttpFiltersChain`, and each reads the same option syntax as mitmproxy:
`[|flow-filter]|subject|replacement`, where the first character is the separator (any character;
`/` in mitmproxy's documentation) and the filter is optional.
```java
bootstrap.withFiltersSource(HttpFiltersChain.of(
BlockList.of("|~d ads\\.example\\.com|404", "|~u /beacon|444"), // 444: close, no response
AntiCache.create(),
MapRemote.of("|https://api\\.example\\.com/|http://localhost:8000/"),
MapLocal.of("|example.com/static/|/srv/static"),
ModifyBody.of("|~s & ~t html|Example Domain|Local Domain"),
ModifyHeaders.of("|~q|User-Agent|MicroProxy", "|~s|Server|"), // empty value: remove
StickyCookie.of("~d example.com")));
```
```bash
java -jar microproxy.jar --mitm --map-remote '|https://api\.example\.com/|http://localhost:8000/' \
--map-local '|example.com/static/|/srv/static' --block-list '|~u /beacon|444' --anticache
```
| addon | command line | what it does |
|---|---|---|
| `MapLocal` | `--map-local [\|filter]\|url-regex\|file-or-dir` | answers from a file, or from a directory: the URL after the match (or the regex's first group) names the file; `index.html` for directories; a `Content-Type` from the extension; `404` when nothing matches; never outside the directory (`..`, absolute parts, links pointing out) |
| `MapRemote` | `--map-remote [\|filter]\|url-regex\|replacement` | replaces matches in the absolute URL (`\1` or `$1` for groups) and sends the request there, with `Host` to match; inside intercepted sessions too |
| `ModifyBody` | `--modify-body [\|filter]\|regex\|[@]replacement` | replaces matches in decoded request and response bodies (`~q` / `~s` pick one side); the replacement is literal, `@file` reads it from a file |
| `ModifyHeaders` | `--modify-headers [\|filter]\|name\|[@]value` | removes the header, then adds the value (none if empty), on requests and on server responses |
| `BlockList` | `--block-list \|filter\|status` | answers with an empty response of that status; `444` closes the connection without one (resets an HTTP/2 stream) |
| `AntiCache` | `--anticache` | removes `If-None-Match` and `If-Modified-Since`, so servers send whole responses |
| `StickyCookie` | `--stickycookie filter` | remembers every `Set-Cookie` (RFC 6265 `Domain`, `Path`, `Max-Age`/`Expires`, `Secure`) in one jar for all clients, and adds the cookies to requests the filter matches, keeping the client's own |
### Filter expressions
**Filter expressions** are a compact subset of mitmproxy's (`org.microproxy.extras.FlowFilter`):
| expression | matches |
|---|---|
| `~u regex`, or a bare `regex` | the URL (absolute; `https://` inside intercepted sessions) |
| `~d regex`, `~m regex` | the host, the method |
| `~h regex`, `~hq regex`, `~hs regex` | a `Name: value` header line of the request or response, the request, the response |
| `~t regex`, `~tq regex`, `~ts regex` | the `Content-Type` of either, the request, the response |
| `~b regex`, `~bq regex`, `~bs regex` | the decoded body of either, the request, the response |
| `~c code`, `~q`, `~s`, `~a`, `~all` | the response status; no response yet; a response; an asset (CSS, JavaScript, images, fonts); everything |
| `!x`, `x & y`, `x \| y`, `(x)` | not, and, or, grouping |
`!` binds tightest, then `&`, then `|`; expressions side by side are joined by "and" with the lowest
precedence, as in mitmproxy (`a b | c` is `a & (b | c)`). Regexes are `java.util.regex`, searched
anywhere and ignoring case; put one with spaces or `()~'"` in quotes. A backslash in quotes only
escapes the quote and itself, so `"\d+"` keeps its `\d` (pyparsing would drop it).
### Cost
The addons read heads only, so bodies keep the proxy's zero-copy relay. A body is buffered
only when a rule might need it, which the heads decide: `ModifyBody` buffers the messages a rule
might edit (and that it can decode; never event streams), and a filter that looks at a body
(`~bq secret`) buffers that body when the rest of the filter could still match. Body filters are
evaluated lazily, with three-valued logic: `~m GET & ~bq x` never buffers a `POST`. Request bodies
over the limit (10 MiB by default, `maxBodySize`) get `413`, as for any filter that buffers
requests; response bodies over it stream through unchanged.
The addon, HAR and replay options are command-line flags (and Java builders); properties files
do not take them.
### Order
The command line chains them as mitmproxy does: `--save-har`, `--warc-dir` and
`--log-http` first, then `--block-list` and `--anticache`, then a `--script`, then
`--server-replay`, `--map-remote`, `--map-local`, `--modify-body`, `--modify-headers` and
`--stickycookie`; the cache comes last. The first short-circuit wins, so a blocked request is never
replayed, and a script sees requests before they are mapped elsewhere.
### Leaving an intercepted session
Requests inside a `CONNECT` that the proxy intercepts normally
go to the `CONNECT` target. When a filter gives one an absolute URI of its own, as `MapRemote` does
and as a script does by assigning `req.uri = "https://other.example/..."`, the request goes to that
server instead: over TLS for `https://` (with the MITM manager's trust), with `Host` set to the new
authority, and on a server connection kept and pooled under the new target. Changing only `Host`
or an origin-form path keeps the request on the session's server. The session's
[ALPN mirroring](https://mahmoudimus.github.io/MicroProxy/guides/mitm/#alpn-mirroring) still applies to the new server, and a plain HTTP session after
`CONNECT` and a [reverse proxy](https://mahmoudimus.github.io/MicroProxy/guides/reverse-proxy/#reverse-proxy-mode) are left the same way. Absolute `https://`
URIs in plain forward requests are fetched over TLS too.
## HAR files
`org.microproxy.extras.HarRecorder` writes the exchanges it sees as an HTTP Archive (HAR 1.2), as
mitmproxy's `--set hardump=` and `save.har` do, for browser developer tools, HAR viewers and
[replay](https://mahmoudimus.github.io/MicroProxy/guides/addons/#replaying-traffic):
```bash
java -jar microproxy.jar --mitm --save-har flows.har # written on exit
java -jar microproxy.jar --mitm --save-har flows.har --save-har-stream --save-har-filter '~d example.com'
```
```java
HarRecorder har = HarRecorder.builder(Path.of("flows.har")).filter(FlowFilter.parse("!~a")).build();
MicroProxy.bootstrap().withFiltersSource(HttpFiltersChain.of(har, otherFilters)).start();
// ... har.close() writes the file; har.save(path) writes a snapshot at any time.
```
- **Entries:** the request as the client sent it (method, absolute URL, version, headers, cookies,
query string, `postData` with form `params`) and the response as the client received it (status,
headers, cookies with `expires`, `redirectURL`, decoded content as text, or base64 with
`"encoding": "base64"` for binary bodies). Also `serverIPAddress`, the client `connection` id,
and `_source` (`server`, `filter`, `cache`, `replay` or `proxy`). Responses from filters, the
cache and replays are recorded too; `CONNECT` tunnels are not. An exchange that ends without a
response has status `0` and an `_error`.
- **Timings** come from [`FlowTimings`](https://mahmoudimus.github.io/MicroProxy/observability/activity-tracking/#observability): `dns`, `connect` (including `ssl`, as HAR
says), `send`, `wait` and `receive`, and `-1` for a phase that did not happen (no lookup or
connect on a reused connection; inside an intercepted session the server handshake belongs to the
`CONNECT`).
- **Bodies** are captured as they stream past, up to `maxBodySize` each (1 MiB by default,
`--save-har-max-body`); a longer one is cut short and marked `_truncated`, and is not replayed.
Capturing bodies turns off the zero-copy relay for recorded exchanges; `content(false)`
(`--save-har-max-body 0`) records heads only and keeps it.
- **Memory:** by default every entry, bodies included, stays in memory until the file is written on
exit (or `close()`), as with mitmproxy's `hardump`: plan for up to twice `maxBodySize` per
exchange (a third more for base64). `stream(true)` (`--save-har-stream`) appends each entry to the
file as it completes and keeps nothing; the file is valid JSON once closed. A name ending in
`.zhar` is written zlib-compressed.
The JSON is written and read by a small package-private writer and parser shared with
`HttpLogger` and `ActivityLogger`; there is no JSON dependency.
## Replaying traffic
### Server replay
**Server replay** (`ServerReplay`, `--server-replay path`) answers requests from recorded
responses instead of contacting servers, as mitmproxy's `server_replay`. Recordings are HAR files
(from `HarRecorder`, browsers or mitmproxy, `.har` or `.zhar`), WARC files (from `WarcRecorder`:
`--server-replay warcs/` takes the whole directory) or both:
```bash
java -jar microproxy.jar --mitm --server-replay flows.har --server-replay-ignore-params _,cb \
--server-replay-extra 404
```
```java
ServerReplay replay = ServerReplay.builder().load(Path.of("warcs")).ignoreHost(true).reuse(true).build();
MicroProxy.bootstrap().withFiltersSource(replay).start();
```
- **Matching** ports mitmproxy's `_hash`: scheme, method, path, request body, host, port and query
parameters (in order) must be equal. `--server-replay-ignore-content`, `-ignore-host`,
`-ignore-port`, `-ignore-params a,b` and `-ignore-payload-params a,b` (form bodies) loosen it;
`--server-replay-use-headers a,b` adds headers. Request bodies are buffered to compare them (16
MiB at most).
- **Use:** recordings answer once each, in recorded order, so a repeated request gets the recorded
sequence of responses; `--server-replay-reuse` lets them answer again and again. Requests without
a recording are forwarded (the default), killed (`--server-replay-extra kill`, the connection is
closed) or answered with an empty response (`--server-replay-extra 404`).
- **Refresh:** `Date`, `Expires`, `Last-Modified` and cookie expiry move forward by the time since
the recording (`--server-replay-no-refresh` keeps them).
- **Bodies:** HAR content is decoded, so it is served without its `Content-Encoding`; WARC bodies
keep the coding they arrived with. The proxy sets `Content-Length`.
- **Source:** replayed answers are `ResponseSource.REPLAY` to trackers, access logs (`REPLAY`),
`HttpLogger` (`source=replay`) and `HarRecorder` (`_source`).
- All recordings are held in memory.
### Client replay
**Client replay** (`ClientReplay`, `--client-replay path`) sends recorded requests again, as
mitmproxy's `client_replay`. They go to the proxy's own listener as ordinary client requests in
absolute form, so every filter, addon, script, cache and recorder applies, and the proxy fetches
`https://` URLs over TLS. `--client-replay-concurrency n` (default 1) bounds the requests in
flight; the proxy stops when they are done, unless `--keepserving` is given. Recorded framing and
hop-by-hop headers are not sent, and a proxy that requires authentication answers them `407`.
```bash
java -jar microproxy.jar --client-replay flows.har --save-har replayed.har # replay, record, exit
```
### With scripts
The command-line addons and recorders compose with `--script`: the script runs
after `--block-list` and `--anticache` and before the other addons (as in mitmproxy), its
`on_request` can move a request elsewhere by assigning `req.uri`, and recorders and loggers placed
first see what the script changed. From Java, put a `ScriptedProxy` anywhere in an
`HttpFiltersChain` with the addons; a script that wants to keep a HAR of its own traffic can be
chained after a `HarRecorder` and the HAR written with `har.save(path)`.
# HTTP cache
Cache responses in memory or on disk as an RFC 9111 shared cache, with revalidation, `Vary`, stale
answers when servers are unreachable and an offline mode. Use this page to speed up repeated
fetches or to browse recorded pages without a network.
`org.microproxy.cache.HttpCache` is a shared cache following RFC 9111. Put it on disk to keep
cached pages across restarts and to browse them offline:
```bash
java -jar microproxy.jar --cache-dir ~/.microproxy-cache --cache-size 2048
java -jar microproxy.jar --cache-dir ~/.microproxy-cache --offline # never contact servers
```
```java
HttpCache cache = HttpCache.builder()
.store(new DiskCacheStore(Path.of("cache"), 2L << 30)) // or new MemoryCacheStore(bytes)
.build();
MicroProxy.bootstrap().withHttpCache(cache).start();
```
What it does:
- **Storing:** complete `GET` responses up to 8 MiB that a shared cache may store. That excludes
`no-store`, `private`, `Vary: *`, responses to requests with `Authorization` (unless
`public`, `s-maxage` or `must-revalidate`), and responses setting cookies (unless `public`).
- **Freshness:** from `s-maxage`, `max-age`, `Expires`, or 10% of the time since
`Last-Modified` (at most a day). Ages follow the RFC, including `Age` and the response delay.
The request directives `max-age`, `min-fresh`, `max-stale`, `no-cache` (and `Pragma: no-cache`),
`no-store` and `only-if-cached` are honoured.
- **Revalidation:** a stale entry with an `ETag` or `Last-Modified` is checked with a conditional
request. A `304` refreshes the entry, and the client still gets the full response.
- **Variants:** `Vary` is matched on normalized request values, and a URL may have several
variants. `HEAD` is answered from a stored `GET`.
- **Invalidation:** a successful `POST`, `PUT`, `DELETE`, etc. removes the URL, and same-origin
`Location` / `Content-Location`.
- **When servers are unreachable:** a stale entry is served instead of the proxy's `502`/`504`,
unless it is marked `must-revalidate`, `proxy-revalidate`, `no-cache` or `s-maxage`. A server's
`5xx` is replaced only within `stale-if-error`.
- With `--mitm`, intercepted HTTPS keeps working: a `CONNECT` to a dead server is still
intercepted, through the `proxyToServerAllowOfflineMitm` filter hook, and the cache answers
inside the session.
- **Offline mode:** everything is answered from the cache, whatever its age; anything else gets
`504`.
- **Reporting:** responses carry `Cache-Status` (RFC 9211), e.g. `MicroProxy; hit; ttl=42` or
`MicroProxy; fwd=stale; fwd-status=304`. `HttpCache.stats()` counts hits, misses, stores,
revalidations and stale responses served.
The cache always runs after the other filters (`withFiltersSource`, `plusFiltersSource`, scripts).
So filters and scripts see a request before the cache can answer it, and the cache stores
responses after they have been rewritten.
Not implemented: `stale-while-revalidate`, `Range` requests (they bypass the cache) and caching
`POST` responses.
# WARC recording
Record the traffic between the proxy and servers as WARC 1.1 files, the format web archives use,
and read them back for replay. Use this page to archive what passes through the proxy; for HAR
files see [Addons, HAR and replay](https://mahmoudimus.github.io/MicroProxy/guides/addons/#har-files).
`org.microproxy.warc.WarcRecorder` records what passes between the proxy and servers as WARC 1.1
files (ISO 28500), the format web archives use. Replay or inspect them with tools such as
[pywb](https://github.com/webrecorder/pywb) and [warcio](https://github.com/webrecorder/warcio).
```bash
java -jar microproxy.jar --mitm --warc-dir warcs # browse; HTTPS is recorded too
warcio index warcs/*.warc.gz
```
```java
WarcRecorder recorder = WarcRecorder.builder(Path.of("warcs")).maxFileSize(1L << 30).build();
MicroProxy.bootstrap().withFiltersSource(recorder).start();
// ... recorder.close() finishes the current file.
```
What gets written:
- **Records:** each exchange becomes a `response` record and a `request` record, linked by
`WARC-Concurrent-To`, with block and payload digests (`sha1:` base32), the target URI
(`https://` inside intercepted sessions) and the server's IP address. Every file starts with a
`warcinfo` record.
- **Files:** each record is its own gzip member, so readers can seek to any record. Files are
named `microproxy--.warc.gz`, carry an `.open` suffix while being written,
and roll over at 1 GiB by default.
- **Bodies:** captured as they stream past, without buffering the exchange (large ones spill to a
temporary file), up to 512 MiB per body. Beyond that the record says `WARC-Truncated: length`.
Chunked transfer coding is removed and `Content-Length` gives the recorded length; content
codings such as gzip are kept as received.
- **Not recorded:** responses from the cache or produced by filters, and `CONNECT` tunnels that
are not intercepted.
- **Ordering:** put the recorder first among the filters, as `--warc-dir` does, to record
messages before other filters change them.
The output is checked with `warcio check` (all digests pass) and indexed by `warcio index`.
`org.microproxy.warc.WarcReader` reads WARC files back, record by record (plain or gzip, one member
per record or one for the file), and `--server-replay warcs` answers requests from the responses
in a directory of them (see [Replaying traffic](https://mahmoudimus.github.io/MicroProxy/guides/addons/#replaying-traffic)).
# Content views, gRPC and protobuf
Render message bodies readably: protobuf without a schema, gRPC message by message with its status
details, JSON, GraphQL, msgpack, forms, Socket.IO and DNS, with field names from a `protoc`
descriptor set. Use this page to read binary traffic in logs, or to decode and edit protobuf and
gRPC from Java; scripts get the same through the [`protobuf` and `grpc`
modules](https://mahmoudimus.github.io/MicroProxy/reference/starlark-api/#protobuf-and-grpc).
## Content views
`org.microproxy.contentviews` turns message bodies into readable text, after mitmproxy's content
views. A `ContentView` has a name, rates how well it suits a body (`priority`, from the
`Content-Type`, the request path and the bytes) and renders it. `ContentViews` holds a set of
views and, in `auto` mode, renders each body with the best view that can; when none can, there
is no rendering and callers show the body as before. `HttpLogger` uses them (see
[Request/response logging](https://mahmoudimus.github.io/MicroProxy/observability/request-logging/)); they also work on their own:
```java
ContentViews views = ContentViews.defaults();
views.render(body, ContentView.Metadata.forResponse(request, response))
.ifPresent(r -> System.out.println(r.view() + ":\n" + r.text()));
```
| View | Bodies | Rendering |
|---|---|---|
| `protobuf` | `application/x-protobuf`, `application/protobuf`, `application/vnd.google.protobuf`, ... | YAML with field numbers (names with a schema) |
| `grpc` | `application/grpc`, `application/grpc+proto`, gRPC-Web (`application/grpc-web`, `-web-text`) | each message, decompressed as `grpc-encoding` says (`gzip`, `deflate`, and `zstd` with `zstd-decoder`), separated by `---`, then the trailers |
| `json` | `application/json`, `text/json`, `+json` | indented by two spaces |
| `graphql` | JSON with a `query` (or a batch of them) | the other members as JSON, then `---` and the query, one field per line |
| `msgpack` | `application/msgpack`, `application/x-msgpack` | YAML: every format of the specification, extensions as `!ext`, timestamps as instants |
| `urlencoded`, `multipart` | `application/x-www-form-urlencoded`, `multipart/form-data` | the fields (a repeated name as a list), files' names, types and sizes |
| `query` | requests without a body | the query parameters |
| `socketio` | paths with `/socket.io/` or `/engine.io/` | one line per Engine.IO or Socket.IO packet |
| `dns` | `application/dns-message` (DNS over HTTPS, and [DNS over QUIC](https://mahmoudimus.github.io/MicroProxy/guides/quic-hooks/#quic-message-framing)'s messages) | as `dig` shows it: the header, then each section's records |
| `hex` | anything, when named | a hex dump |
`views.with(myView)` adds a view (or replaces one of the same name); a view that fails on a body
is skipped. Views get bodies with their `Content-Encoding` removed, and must throw nothing but
`DecodeException` for data they cannot render.
### Protobuf without a schema
```text
1: 150 # !sint: 75
2: hello
3: !fixed32 3.14159 # u32: 1078530000
5:
1: 42 # !sint: 21
6: !binary 038e029ea705 # packed: [3, 270, 86942]
```
The decoding follows mitmproxy's (ported from mitmproxy_rs):
- **Numbers.** Varints and fixed-width values are what the wire says; each is shown in its
shortest reading, with the others as a comment: unsigned, signed when the top bit is set (a
negative `int32` or `int64`, which take ten bytes), zigzag (`!sint`, for `sint32` and
`sint64`), and as a float or double for `!fixed32` and `!fixed64`. Which one the field really
is cannot be told from the bytes.
- **Length-delimited values** are guessed per field number, over all of the field's values:
text when every value is UTF-8 without control characters (other than whitespace); else a
nested message when every value parses as one (minimal varints, field numbers in range, one
wire type per field number); else bytes, with a `packed:` comment when they read as two or more
packed varints. Ambiguity is inherent: `08 96 01` is a message with field 1 = 150 and also the
packed values 8 and 150; the message wins, as in mitmproxy.
- **Repetition.** A field that occurs more than once is a list. Groups (wire types 3 and 4) are
`!group` mappings. Fields are sorted by number.
- **Limits.** Messages nest at most 64 deep (deeper values stay bytes), groups too (deeper ones
are an error), and lengths are checked against the input before anything is allocated. Bad
input is a `DecodeException`, never anything else; the build fuzzes the decoders with a fixed
seed.
The same in Java, with editing:
```java
ProtoMessage m = Protobuf.decode(bytes);
Map