Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 04:55
· 61 commits to main since this release

ONRouter v0.5.0 — the release where the CDN/front-proxy layer landed with no provider
knowledge inside the binary
, and where a feature's cost is measured against its own control
row instead of asserted. Everything new is default-off, memory-bounded, and proven with real
traffic against a spawned gateway.

Front proxy, configured rather than compiled in

front_proxy <name> { … } blocks declare a probe header, an ordered real-client header chain,
role headers for scheme/country/host, and the ranges that peer is that fleet. The engine never
names a provider: grep -a -i -o over the release binary finds 0 hits for Cloudflare,
Fastly, CloudFront, Akamai or any of their header names, while the controls
front_proxy/auto_trust hit 48/10 times and the only baked-in header names are nginx's own
x-real-ip (1) and x-forwarded-for (2). A provider renaming a header or rotating its ranges is
a config edit, not a code path.

Recognition never grants trust. Every header-derived value requires the direct TCP peer to sit
inside profile.trust ∪ set_real_ip_from, and promoting a profile's own ranges into a rewriting
authority root takes the explicit front_proxy_auto_trust on, which -t and startup both name
the vhost for. egress mode then moves $remote_addr (so limit_req, geo, ACLs, VIP
reputation and the access log all key on the true client), restores the visitor-side $scheme,
and emits RFC 9209 Cache-Status: onrouter; detail=… for verified peers only. Four "declared but
inert" shapes are -t errors, not warnings.

conf/front_proxy_examples.conf is the operator-facing half: copy-paste profiles for a
Cloudflare-shaped edge (22 published ranges, refreshed from the provider's own list, with the
commands to re-fetch), a CloudFront-shaped one (0 ranges shipped on purpose — pull the
origin-facing prefix list at deploy time) and a plain-XFF self-built edge, plus a detect server
that boots as-is and an egress upgrade template.

Learning trio — one ledger, all off by default

proxy_cache_learning (TinyLFU admission + eviction), limit_req_learning (per-key burst shape
feeding queue/delay) and vip_learning (reputation → queue priority + cache residency) share one
accounting ledger under learning_memory_budget. Attack surface was designed in: a forged
realip report cannot buy VIP weight.

Surviving an upstream outage

still_online.rs replays a last-known-good response when the upstream is down, behind a
credential gate that cannot be switched off — no Set-Cookie/Authorization-bearing response is ever
replayed to a different principal — with a single-flight background retry once health returns.

Cache / disk tier / config semantics

Cold tier under ngx.shared.DICT (disk_cache.rs) persists asynchronously and rebuilds its index
by scanning the directory, so recovery does not depend on a sidecar manifest; the Lua API is
unchanged. proxy_ignore_headers, empty-and-multi-value proxy_no_cache/proxy_cache_bypass,
add_header with an undefined variable (omitted, not printed literally), $upstream_cache_status,
and the REWRITE-phase set + if subset (=, !=, ~, ~*, !~, !~* wrapping
rewrite/set/return, config order preserved; nested if, file tests and if without a block
are parse-time errors).

Performance: the diagnostics stopped paying for themselves

All four log macros expanded to with_log(level, format!(…)), and format! is an argument — it
ran unconditionally and threw the String away under the default warn threshold, so a
per-request diagnostic was a real cost. They now test log::enabled(level) first. Measured with
bench_front_proxy.py (one binary, five identically-shaped vhosts + a floor gateway, branch
self-proof per row, 5 passes taking the median), read as each row against its own off row
because the floor row's own round spread reached 36%: the egress penalty at c=1 went from
−0.4%…−9.2% to −0.4%…−4.8% (cache-hit row −9.2% → −0.4%), c=32 stayed inside jitter, and the
off/detect/notrust control rows did not move. detect costs nothing measurable — it returns
after labelling, before address parsing. What remains on the egress path is per-request
String/Vec cloning, which is the next target.

Security

docs/SECURITY_AUDIT.md records S9-S12, including the front-proxy trust boundary and the fact
that a stale or too-wide trust range is an operator-owned snapshot the engine cannot validate.
rce_matrix 27/27: a .lua file or the config has code-execution power, and a script that
concatenates ngx.var.arg_* into load() / os.execute() is an application-level RCE the
gateway will not contain.

Verification

  • cargo test --workspace 387/387 (374 onrouter-server + 13 onrouter-config).
  • 74 Python E2E scripts (tools/*_e2e.py + 8 root scenario suites), all exit 0 with real traffic
    against a spawned gateway and fake backends: python run_regression.py →
    TOTAL 74 scripts, nonzero=0 (2026-10-20). Highlights: front_proxy 54/54,
    front_proxy_examples 24/24 (boots the shipped vendor example and proves the
    recognise-without-trust split at runtime), disk_cache 53/53, vip_learning 51/51,
    queue_learning 42/42, metrics 38/38, security_matrix 36/36, learning_cache 34/34,
    still_online 25/25, set_if 26/26, api_compat 57/57, rce_matrix 27/27.
  • Binary string audit for vendor names/headers, re-run against this 0.5.0 build.

Assets

Eight cross-platform archives (Linux x86_64/aarch64/armv7/riscv64 as .tar.gz, macOS x86_64/aarch64
and Windows x86_64/aarch64 as .zip), each containing onrouter + orr-gencert, plus
SHA256SUMS.txt whose digests verify against the release's flat asset names — checked against
GitHub's own server-side digest for every asset: 8/8 identical.

The CI-built Windows x86_64 asset was extracted and driven with real traffic, not just
inspected: --version and the response Server: header both report ONRouter/0.5.0,
-t conf/front_proxy_examples.conf exits 0, a static file is served (200, 2694 bytes), a full
Cloudflare-header request through that binary is labelled X-Fp: cloudflare-edge while
$remote_addr stays 127.0.0.1 (recognise ≠ trust holds in the shipped artefact), and the
vendor-string audit over the packaged binary is again 0 hits with front_proxy at 48 as control.

Full Changelog: v0.4.0...v0.5.0