Forward proxy¶
The default mode: clients are configured to use MicroProxy as their HTTP proxy and send it
absolute-form requests and CONNECTs. 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.
Running a forward proxy¶
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
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. 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), and when the proxy has to answer itself it sends a short plain-text answer (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). 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 <file> and --listener-keystore-password <pw> (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).
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). 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 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 <read> <write>, 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 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-allincluded; 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 isProxyFailure.TlsFailed: a502from the failure responder, theUFresponse flag withproxy_failure:tls_faileddetails, the handshake's error in%UPSTREAM_TRANSPORT_FAILURE_REASON%(an untrusted issuer, a name the certificate does not cover; see Server TLS failures),on_failurein scripts, and the cluster'sssl.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). - 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 and
pool_shared_mitm_connections, every client shares them, whether they were made for an intercepted session or for a client'shttps://URI. - HTTP/2 and HTTP/3. Unless
--no-http2-upstream, the handshake offersh2andhttp/1.1, and a server that picksh2gets the requests as streams on a shared connection, with 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, andUpstreamProxyManagerpicks its HTTPS upstream (--upstream-https-proxy) for anhttps://URI. - What everyone sees. Filters get the request as the client sent it,
https://...included; scripts seereq.scheme == "https"and thehttps://URL in every hook; the server gets it in origin-form withHostfrom the URI. Access logs record thehttps://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) sayhttps.
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:
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-binandsentry-trace.X-Request-Idis not on it, because servers often want it; add it withplusStrippedRequestHeadersif 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 andActivityTracker.requestReceivedFromClientstill see them;requestSentToServerdoes not. - Where: plain requests, requests inside intercepted (MITM) sessions, WebSocket upgrade
requests, and the
CONNECTrequests 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-headersand--strip-request-headers a,b; in properties files:strip_tracing_headers=trueandstrip_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) andwithMaxConnectionsPerHost(default 10). When the pool is full, a request waits up to the connect timeout for a connection, then gets503. LittleProxy returned502immediately. - Idle eviction:
withPoolIdleTimeoutcloses 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). - Metrics:
HttpProxyServer.getServerConnectionPoolMetrics()reports counts.