EQProxy Load balancer and reverse proxy in Rust

Converters

eqproxy config import reads an nginx, HAProxy, ingress-nginx or Envoy configuration and writes an equivalent eqproxy.toml plus a report that classifies every source directive as converted, to review or rejected.

It is static analysis: it runs nothing from the source configuration. And the result is parsed and validated with EQProxy's own parser before being delivered, so the converter cannot return a file the product would reject.

How it is invoked

eqproxy config import \
  --input haproxy.cfg \
  --output eqproxy.toml \
  --report report.md
OptionUse
--from nginxnginx, haproxy, ingress (or ingress-nginx) or envoy; if omitted, it is detected from the content
--inputSource file
--outputDestination of the eqproxy.toml
--reportDestination of the Markdown report

Exit codes:

CodeMeaning
0Clean conversion: every directive has a destination or is informational
2The generated file is valid, but there are rejected directives: the report names them
1Nothing was written: the source could not be read or the result did not pass validation

The report opens with the counts —directives read, classified, converted, with warning and rejected— and with two tables of what was generated (listeners and backends). Then it lists the directives by severity: no traducible, rechazado (sin equivalente), revisar, informativo and convertido. It closes with “What has not been translated” and the next step, which is always eqproxy config validate on the output.

What is converted

The table describes the equivalent form in each dialect; the converter generates it when it recognizes the construction and, when it does not, names it in the report. nginx's if is the typical case: it is not translated on its own.

PieceHAProxynginxEnvoyingress-nginxEQProxy
Prefix routeuse_backend … path_beglocation /prefix/prefixpath Prefixpath_prefix + use_backend
Exact route{ path /x }location = /xpathpath Exactpath
Hosthdr(host)server_namevirtual_hosts[].domainsIngress hosthost
Method{ method POST }if ($request_method …):method headersnippetmethods
Canary by cookiecook(canary) -m str onmap $cookie_…regex on cookiecanary-by-cookiecookies
Canary by headerhdr(x-canary)map $http_x_canaryexact_matchcanary-by-headerheaders
Prefix rewritehttp-request set-pathrewrite … breakprefix_rewriterewrite-targetrewrite.path_prefix_rewrite
Redirecthttp-request redirectreturn 301redirectsnippetredirect
Local responsehttp-request returnreturn 200direct_responsesnippetdirect_response
Default backenddefault_backenddefault_serverdomains: ["*"]defaultBackendthe listener's backend

Also: servers with weight, marked backup and with maxconn; TCP and HTTP health checks; retries; maxqueue; affinity by cookie or by IP; slow_start; option forwardfor; client and backend TLS; errorfile/errorloc; CORS; rate limit; body and header limits; timeouts; gzip; and map/geo as [maps.*].

What is not converted

SourceLeft out
nginxfastcgi_pass, uwsgi_pass, scgi_pass, memcached_pass; static files (root, alias, try_files, index); set and variables; if; proxy_method; proxy_buffering; limit_rate; ssl_ciphers; stream and ssl_preread
HAProxylua-load; stick-table as a store; tcp-request/tcp-response; transparent and source; peers; external-check and agent-check; program, mailers, userlist
ingress-nginxconfiguration-snippet (never executed); ssl-passthrough; objects that require watching the API server; UDP services
Envoydynamic resources (cds_config, xDS); Lua filters; outlier detection and circuit breakers; udp_listener

The source's certificate paths are not copied: they have to be adjusted by hand in the generated eqproxy.toml.

Real example: nginx before, EQProxy after

Source, taken from docs/migration/nginx.conf (the storefront vhost):

server {
    listen 8080;
    server_name shop.example.com;

    location = /healthz {
        return 200 "ok";
    }
    location /admin {
        if ($request_method = POST) { proxy_pass http://172.24.0.13:80; }
        proxy_pass http://172.24.0.11:80;
    }
}

Translation, taken from docs/migration/eqproxy.toml:

[[listener.web.rules]]
name = "healthz"
path = "/healthz"
[listener.web.rules.direct_response]
status = 200
body = "ok"

[[listener.web.rules]]
name = "admin-writes"
host = "shop.example.com"
path_prefix = "/admin"
methods = ["POST"]
use_backend = "admin"

[[listener.web.rules]]
name = "admin-reads"
host = "shop.example.com"
path_prefix = "/admin"
use_backend = "web"

nginx's if ($request_method = POST) has no direct translation: the converter classifies it as a directive with no equivalent and refers to the rules. The migration configuration writes it as two ordered rules —first the POST one, then the generic one— because the first match decides and there are no conditional blocks.

The equivalent command, with the report alongside:

eqproxy config import --input nginx.conf --output eqproxy.toml --report report.md
eqproxy config validate --config eqproxy.toml

A clean conversion (exit code 0) does not promise functional equivalence: it promises that no directive was discarded silently. Routing equivalence is demonstrated separately, case by case, in the parity matrix.