03. Internal Architecture
Ferrix is engineered around modern distributed systems principles and asynchronous network I/O. This section explains the internal components and how they cooperate to deliver high throughput and zero-downtime operation.
🏛️ Structural Overview
┌────────────────────────────────────────────────────────┐
│ FERRIX PROCESS │
│ │
Incoming HTTP / WS ─────►│ Proxy Listener (Port 80/443, configurable) │
│ ├── Client Address Extractor (ConnectInfo) │
│ ├── Host Header Matcher (VirtualHost Finder) │
│ ├── Middleware Pipeline (IP ACL, RateLimit, Auth) │
│ ├── Route Rule Prefix & Regex Matcher │
│ │ ├── Target: Static File Server (Zero-Copy) │
│ │ ├── Target: HTTP Redirect (301/302/307/308) │
│ │ └── Target: Upstream Pool (Load Balancer) │
│ │ ├── WS Upgrade: Tokio Bidirectional Tunnel │
│ │ └── HTTP: Hyper / Reqwest Client + Failover │
│ │ │
│ └── Telemetry Interceptor │
│ ├── Atomic Counters (2xx, 3xx, 4xx, 5xx, Bytes) │
│ ├── Lock-Free Ring Buffer (Recent 1000 logs) │
│ └── Broadcast Channel (tokio::sync::broadcast) │
│ │ │
│ ▼ │
Browser / REST API ─────►│ Admin Listener (Port 8081 / Configurable) │
│ ├── REST API Handlers (/status, /config, etc.) │
│ ├── Server-Sent Events (SSE Stream) │
│ └── Embedded React SPA (rust-embed Assets) │
└────────────────────────────────────────────────────────┘
⚡ Zero-Downtime Hot Reload via ArcSwap
In traditional Nginx, reloading configuration without stopping the server (nginx -s reload) relies on the master process spawning new worker processes and waiting for old ones to terminate active connections (graceful worker restart).
In Ferrix, taking advantage of Rust's atomic pointer primitives, we use the arc-swap crate (ArcSwap<FerrixConfig>):
- The runtime configuration is wrapped in an atomic pointer:
#![allow(unused)] fn main() { pub shared_config: Arc<ArcSwap<FerrixConfig>> } - During request processing, Tokio worker threads perform a quick lock-free load (
shared_config.load()), requiring only a single atomic CPU instruction (no mutex locks, zero lock contention). - When new configuration is submitted through the web dashboard or REST API:
- The configuration is validated;
- Persisted safely to
ferrix.yamlon disk; - Stored atomically into the pointer via
shared_config.store(Arc::new(new_config)).
- All incoming connections immediately see the updated routing table, while in-flight requests complete uninterrupted.
- Result: Zero downtime, zero dropped connections, and sub-millisecond reload latency.
🔄 Full-Duplex Bidirectional WebSocket Tunnel
When a client sends a request with Upgrade: websocket and Connection: Upgrade headers, Ferrix automatically initiates a protocol upgrade:
- The initial handshake is processed via Axum's
WebSocketUpgrade. - Concurrently, Ferrix establishes an asynchronous connection to the target backend chosen by the load balancing algorithm using
tokio_tungstenite::connect_async. - Once connected, two concurrent asynchronous tasks bridge the client and upstream:
- Task A: reads frames from the client and forwards them to the backend.
- Task B: reads frames from the backend and relays them to the client.
- When either peer sends a
Closeframe, the tunnel terminates cleanly.
📊 Telemetry, Metrics, and Server-Sent Events (SSE)
The telemetry subsystem is designed to introduce zero overhead to the critical request path:
- Atomic Counters:
total_requests,active_connections,status_2xx,status_3xx,status_4xx,status_5xx, andtotal_bytes_sentutilizeAtomicU64with relaxed ordering (Ordering::Relaxed). - Latency Percentiles (p50, p95, p99): Computed over a circular ring buffer tracking the last 1,000 requests.
- Real-Time Streaming: Each finished request emits a
RequestLogto an asynchronous broadcast channel (tokio::sync::broadcast::Sender). - SSE Endpoint (
/api/logs/stream): The React dashboard consumes this event stream overEventSource, updating the live requests table without polling.
📁 Source Code Organization
The core follows the same layering as nginx's src/ directory: a base layer with no
HTTP in it, the event loop, the HTTP server split into small modules (one per nginx
module it mirrors), and the OS-specific bits apart.
core/src/
├── main.rs process bootstrap, CLI, signals, worker threads (nginx.c, os/unix/ngx_process*)
├── config/ JSON config model, validation, hot-reload store (ngx_conf_file + each module's directives)
├── event/
│ ├── accept.rs accept loop, worker_connections, hyper handoff (event/ngx_event_accept.c)
│ └── clock.rs cached time + per-worker coarse timers (core/ngx_times.c, event/ngx_event_timer.c)
├── os/ RLIMIT_NOFILE, CPU affinity (os/unix)
├── tls/
│ ├── mod.rs which ports speak TLS
│ ├── sni.rs certificate per SNI name, rustls acceptor (event/ngx_event_openssl.c, ngx_http_ssl_module)
│ └── acme.rs Let's Encrypt certificates
├── http/
│ ├── mod.rs request lifecycle: vhost → route → handler → filters (ngx_http_core_module)
│ ├── server.rs Ferrix's own HTTP/1.1 parser/writer, sendfile (ngx_http_request.c, ngx_http_parse.c)
│ ├── matcher.rs server_name and location matching (ngx_http_core_module locations)
│ ├── proxy.rs reverse proxy handler, failover (ngx_http_proxy_module)
│ ├── static_files.rs static files, ETag/304, ranges, precompressed copies (static, not_modified, range, gzip_static)
│ ├── cache.rs response cache (proxy_cache)
│ ├── access.rs allow/deny + basic auth (access, auth_basic)
│ ├── limit_req.rs rate limiting (limit_req)
│ ├── limit_conn.rs connection limiting (limit_conn)
│ ├── realip.rs client IP from trusted proxies (realip)
│ ├── websocket.rs WebSocket tunnel (proxy upgrade)
│ ├── default_page.rs "Welcome to Ferrix" page (default site)
│ ├── filters/ response filter chain, run in order: (header/body filters)
│ │ compress → headers → cors → request_id, plus error_page
│ └── upstream/
│ ├── mod.rs client to upstreams (timeouts, retries on stale conns) (ngx_http_upstream.c)
│ ├── h1.rs HTTP/1.1 client with a per-thread connection pool (upstream_keepalive)
│ ├── balancer.rs round robin, weighted, least_conn, ip_hash, random (upstream_*_module)
│ └── hash.rs consistent hashing (ketama) (upstream_hash)
├── stream.rs TCP/UDP proxy (src/stream/)
├── health/ active health checks
├── metrics/ counters, latency histogram, request log, Prometheus (stub_status, much extended)
├── admin/ control-plane REST API and SSE
├── net/ CIDR matching
└── nginx/ nginx.conf importer (lexer, parser, converter, report)
Adding an HTTP feature: a new response filter goes in http/filters/ and is called
from filters::run in the right order; a new content handler gets its own file under
http/ and a RouteTargetType; a new balancing algorithm goes in
http/upstream/balancer.rs.