EQProxy Load balancer and reverse proxy in Rust

Parity

The parity matrix classifies the 120 reference rows of HAProxy 3.4.6 LTS, nginx 1.27.1, ingress-nginx and Envoy API v3: 61 from HAProxy, 20 from nginx, 19 from ingress-nginx and 20 from Envoy. None is left unclassified, and no classification rests on "it compiles, therefore it works".

v0.9.1 result: 11 E1 · 88 E2 · 21 E3 · 0 unclassified. That is not full parity and the page does not claim it.

What each class means

ClassMeaningEvidence required
E1Same observable semantics as the referenceA differential corpus case that runs against the reference and EQProxy and passes
E2Equivalent with a known and documented differenceA test of the behavior and the difference named in the row
E3Out of scope by decisionA public ADR and an alternative

An E1 is only accepted if the differential corpus runs it: it is not inferred from a unit test or from the code looking equivalent. A row whose shared semantics are covered but whose directive set is not keeps the E1 for what is covered and names what is missing in the difference column.

Count by reference

ReferenceRowsE1E2E3
HAProxy 3.4.6613499
nginx203152
ingress-nginx193115
Envoy API v3202135
Total120118821

The eleven E1 rows are: host, path, header and method routing, URL and header rewriting and redirects in HAProxy (C3, C4, C5); virtual hosts, location and headers with X-Forwarded-For in nginx (B-N2, B-N3, B-N4); host and path, PROXY and X-Forwarded-*, and redirects and rewrites in ingress-nginx (B-I2, B-I4, B-I6); advanced routing and header manipulation in Envoy (B-E3, B-E17).

What is out of scope

What EQProxy does not do, by decision written in an ADR:

Known gaps and narrower semantics

In addition to the above, v0.9.1 has limits that are worth reading before testing anything:

PointStatus
HTTP/3Experimental: browsers and curl over h3 unverified, and the threat model's QUIC adversarial tests have not been run
UsersNone outside the author; production phase 9 is still pending
mapOnly equals matching
error_pages with format = "raw"Keeps the status, the Content-Type and the body; the generated Date replaces the file's
Date of an origin that does not send itRelayed as is: the proxy only stamps the responses it generates itself
Interim 100 Continue responseWithout header fields
CacheWithout stale-while-revalidate or stale-if-error
macOS binariesNot static; only the Linux ones are

How the evidence is generated

Two Rust harnesses, both #[ignore] because they need Docker.

Differential corpus (23/23). Starts HAProxy 3.4.6, nginx, the ingress-nginx dataplane and static Envoy in containers, and compares each case against EQProxy: status, Location (path and query), declared bodies and declared headers, after the documented normalization. It does not compare times, connection reuse, TLS parameters or bytes of error pages.

cargo test -p eqproxy-cli --test differential -- --ignored --nocapture

Migration matrix (65/65). Four hand-written source configurations and their translation to eqproxy.toml route case by case the same: 13 declared cases × 5 implementations. The originals run in containers and the eqproxy binary natively, all in front of the same five echo backends.

cargo test -p eqproxy-cli --test migration -- --ignored --nocapture

A green corpus is the absence of a counterexample, not a proof of equivalence. The migration matrix is 13 declared cases, not a proof for arbitrary configurations: the method and its limits are published alongside the evidence.