Repository navigation
v0.4.0
ONRouter v0.4.0 — the project is renamed from LearnON, this carries the nginx/OpenResty parity
campaign that followed v0.3.0, and it is the first release with a documented security audit
(docs/SECURITY_AUDIT.md, findings S1-S8) where every finding was reproduced with real traffic
before it was fixed.
Naming
onrouter,orr-gencert,orr-h3checkbinaries; cratesonrouter-config/onrouter-server;
conf/onrouter.conf;ONROUTER_*env prefix;Server: ONRouter/0.4.0;onrouter_*metric names.
Request path
- Upgrade (WebSocket) tunnels now pass the ACCESS-phase controls (
allow/deny,auth_basic,
limit_req/limit_conn) and honourproxy_read_timeoutas an idle timeout. client_max_body_sizedefaults to1m; streamed chunked bodies are length-capped and forwarded
without copying the whole body;client_body_timeoutapplies to streamed uploads.- Upstream response bodies are read under
proxy_read_timeoutwith the connection guard alive for
the whole transfer, so passive health checks and metrics count aContent-Lengthresponse on its
last frame.
Proxy / cache / TLS
proxy_send_timeout,proxy_next_upstreamtries+deadline budgets,balancer_by_luapeer and
timeout overrides, per-status cache counters,proxy_cachelock/stale/bypass with LRU eviction.- Session ticket +
ssl_session_cachesemantics,ssl_certificate_by_lua, hot cert reload on SIGHUP,
mTLS to upstreams,ssl_protocolsselection with optional--features native-tlsfor TLS 1.0/1.1. - L4
stream {}proxying withssl_prereadSNI routing, connection accounting and graceful drain.
Lua surface
receiveuntil, timer coroutines,ngx.sleep/cosocket/ngx.req.socket/ngx.flush,ngx.var
read+write,ngx.re.*,ngx.balancer,init_worker_by_lua+lua_package_path, bundled
resty.cookie,resty.limit.{req,count,conn},resty.http/resty.redis, concurrent subrequests
withngx.semaphore, andshared.DICTaccounting withmake_room.- Cooperative preemption: every coroutine resume arms a Lua count hook
(lua_slice_instructions, default 1 000 000 instructions ~ 10 ms), so a runaway loop can no longer
blackout the single process.
Observability
/metricsgained per-upstream node connections/health/success/failure, cache status, TLS handshake
failures, limit rejections and dropped access-log lines; the combined log carries$request_time
plus$upstream_addr/$upstream_status/$upstream_header_time.
Security audit (S1-S8)
Each row was reproduced against the pre-fix binary first, and each has a probe that fails there and
passes here (tools/security_matrix_e2e.py 20/20, tools/rce_matrix_e2e.py 27/27):
- S1
auth_basicwith no user file served the location without any credential — now 401 +error_log. - S2 an htpasswd line with an empty hash authenticated on an empty password — such entries are dropped.
- S3 the htpasswd cache keyed on mtime only, so rewrites that preserved mtime kept revoked users
logged in — now keyed on(mtime, len)and bounded to 1 s. - S4 the Upgrade request line was built from the decoded path (
/ws%23fragreached the backend as
/ws#frag), a routed-vs-forwarded desync — the raw undecoded request target is forwarded now. - S5
proxy_request_buffering offskippedclient_body_timeout, so a stalled uploader pinned a
connection and an upstream socket forever — a per-frameSleepguard rearms on every data frame. - S6 any
sub_filtermade the static handler read the whole file into memory (32 MiB x concurrency,
switched on by one directive) — only the requested window is read, and above 8 MiB it streams
unfiltered with a logged reason. - S7
while true do endincontent_by_lua_blockheld a VM thread and, because ONRouter is one
process, unrelated endpoints stopped answering — bounded by the preemption quantum above. - S8
run_phasessignals errors withtrue, but every caller testedif !run_phases(...), so a
missing/brokenaccess_by_lua_fileread as "check passed" and the protected body was served with
200; a streamingcontent_by_lua_filethat failed to load produced an empty 200. Callers
now terminate on error (nginx parity: an unreadable phase file is a 500, never a pass).
Remote-execution surface
There is no path from client input to code execution: config _by_lua_* bodies and
_by_lua_file paths are config-time literals (no request-variable interpolation, so ?f= cannot
redirect a script path), every lua.load() in the gateway compiles a constant library source, the
debug library is unavailable so mlua's named registry (where cross-request handles live) is
unreachable, there is no process spawn or request-driven file write in the shipped path, no
X-Accel-Redirect, proxy_pass targets cannot be steered by $arg_/request headers (verified
against a canary listener), encoded CR/LF is answered with 400, and internal locations stay
unreachable externally across nine encodings.
Trust boundary, stated plainly: Lua scripts are not sandboxed — os, io, load, loadfile,
dofile, require and package are available inside a script, exactly as in OpenResty. Anyone who
can write 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. Treat config and scripts as trusted code.
Verification
cargo test --workspace255/255 (242 onrouter-server + 13 onrouter-config).- 65 Python E2E suites, all exiting 0 with real traffic against a spawned gateway and fake backends
(python run_regression.py summaryE.txtwrites the exit-code roll-up underlogs/regression/);
highlights:rce_matrix27/27,security_matrix20/20,api_compat57/57,metrics38/38,
observability26/26,lua_flush_socket23/23,thread24/24,subrequest18/18,static_stream19/19. - Windows A/B vs OpenResty (
bench_ab.py --sweep 1,4 --requests 12000, 16 logical cores, keep-alive
GET without TLS): static 1.26x / 2.22x, proxy 2.96x at c=4, Lua 0.71-0.89x of LuaJIT. Read
docs/OPENRESTY_GAP_ANALYSIS.mdsection 3 for the caveats — no Linux baseline has been recorded yet.
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. The Windows x86_64 asset was verified by extracting it and driving real requests:
--version reports ONRouter/0.4.0, the response carries Server: ONRouter/0.4.0, and the S8
fail-closed behaviour is present in the packaged binary.
Full Changelog: v0.3.0...v0.4.0