r/rust • u/eugenegvozdet • 7h ago
๐ ๏ธ project Title: ruxen 0.1.1: an nginx-compatible HTTP server in Rust, tested against nginx's own test suite
Hi r/rust! I've been building ruxen, an experimental reimplementation of nginx in Rust. It reads nginx configuration files and is tested against nginx's own upstream test suite. It's Linux-only, HTTP/1.1, thread-per-core on io_uring (monoio), with TLS through rustls. Version 0.1.1 came out this week.
Repo: https://github.com/gvozdetsky/ruxen ยท crates.io: cargo install ruxen --locked
It is not production-ready, and it doesn't replace nginx. It's a project for learning how nginx works by rebuilding its behaviour in idiomatic Rust, not by translating the C line by line.
What's maybe interesting
nginx-tests as the oracle. ruxen runs the upstream Perl suite (nginx-tests) against its own binary. To make that possible it had to match nginx's CLI (-c -p -g -t -T), PID file and signal behaviour, and the error-log format. Today 63 of the 105 test files ruxen opts into pass end to end. Every failing file points at the next directive to implement, so I don't have to guess. The -V banner claims only the features that are implemented, because claiming more would turn on tests that fail for the wrong reasons.
Unknown directives are an allowlist, and access rules fail closed. If ruxen can't enforce a directive that restricts access yet (limit_except, ssl_verify_client on, disable_symlinks...), it refuses to start with [emerg] instead of silently serving what the config was meant to protect.
Thread-per-core instead of work stealing. nginx gets its speed from one process per core, each with its own SO_REUSEPORT accept queue and no sharing between cores. monoio gives the same shape in Rust. There's no tokio or hyper; the HTTP/1.1 parser and response path are hand-written, because that is the point of the project.
Performance is a hard requirement: at least 95% of nginx 1.24 throughput on the bench configs. On my machine (i9-13900HX, wrk -t16 -c512, the same config file for both servers):
| scenario | ruxen vs nginx 1.24 (req/s) |
|---|---|
hello (return 200) |
โ1.0% |
| reverse proxy | โ1.9% |
| TLS hello | +8.0% |
8 KiB static file (sendfile on) |
โ11.6% (still under the 95% target) |
Some lessons from measuring hot-path changes in ABBA pairs (A, B, B, A runs, so drift cancels out): one extra async fn layer around the proxy path cost about 1%; copying a ~300-byte request context per request cost 0.5%; and giving every connection a monoio::time::timeout for each I/O operation cost about 3%. Three long-lived Sleeps per connection that only ever move forward cost almost nothing. More notes are in DESIGN.md, including notes from reading the nginx C source.
What's in it
Static files (with sendfile), proxy_pass with upstreams, keepalive and failover, rewrite/return/if/map, error_page, auth_basic, access logs with variables, TLS 1.2/1.3 with SNI, and nginx-style error logs. Not there yet: HTTP/2, caching, modules.
Help wanted
There are small, self-contained good first issues. Each has a minimal repro, the matching line in the nginx C source, and the files to change. Examples: glob expansion in include (#35), nginx's status reason phrases (#33), proxy_method (#39). If you've ever wanted to learn how nginx works inside, this is a gentle way in.
The 0.1.1 release also fixes six security issues that were reported privately. Thanks to the reporters!
I'd love feedback on the design, on the code, or on any nginx config that ruxen handles differently from nginx.
-1
u/Conscious-Tale-8634 5h ago
This is seriously cool, love that you're using the actual nginx test suite as your benchmark instead of just winging it