- Rust 64.1%
- Svelte 28.3%
- TypeScript 2.7%
- Just 2.1%
- CSS 1.6%
- Other 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| crates | ||
| docs | ||
| examples/wasm | ||
| site | ||
| src | ||
| ui | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| justfile | ||
| README.md | ||
| redocly.yaml | ||
Raahi
Raahi means traveler or wayfarer in Hindi and Urdu. It is a fast, self-hosted reverse proxy and API gateway for HTTP, HTTPS, WebSocket, and TCP traffic.
Raahi routes requests to healthy upstreams and handles TLS, load balancing, authentication, rate limits, caching, traffic splitting, and request transforms. Manage it through the web UI or REST API. Route, policy, target, and certificate changes take effect without restarting the proxy.
┌──────────────────────── raahi (one process) ───────────────────────┐
client ───▶│ Pingora data plane ── reads ──┐ │
:8080/:8443│ route → plugins → LB → upstream │ ArcSwap<Config> (hot reload) │
admin ───▶│ axum admin API + UI ── writes ─┴──▶ SQLite ──── rebuild snapshot │
:9080 │ active health checks │
└─────────────────────────────────────────────────────────────────────┘
Features
-
Add application: a two-step setup/review flow creates a service, target, domain route, and optional HTTPS redirect, HSTS, and IP allowlist together. Test upstream connectivity before saving. HTTPS reuses an existing matching certificate; drafts survive visiting Certificates or Settings. Creation is transactional and reloads once.
-
Routing by host (exact +
*.wildcard), path prefix (longest-match) or~-prefixed regex paths (anchored at the path start;strip_pathstrips the matched portion), method, and header conditions (exact value or presence), with priorities. -
Traffic splitting / canary: routes can split across services by weight (weighted round-robin per request).
-
Load balancing across weighted targets: round-robin, weighted, random, consistent-hash (by client IP).
-
L4 stream routes: raw TCP listeners spliced to services (databases, Redis, any TCP protocol) reusing the same load balancing and health flags, with per-listener connection/byte metrics. Retargeting applies live; listener add/remove needs a restart.
-
Path & host rewriting (
strip_path,preserve_host) andX-Forwarded-*injection. -
Built-in plugins (native Rust; scoped global / per-service / per-route):
- Auth:
key-auth(header or query),basic-auth(bcrypt), andjwt(HS256/384/512 + RS256, per-consumer credentials looked up by key claim) - Access:
acl(consumer-group allow/deny) andip-restriction(CIDR allow/deny) rate-limit(sliding window, keyed by IP / consumer / route,RateLimit-*headers, counters survive config reloads)- Traffic:
request-termination(maintenance mode),request-size-limit(413), andredirect(301/302/307/308 with path preservation) cors(preflight + response headers)request-id(correlation id: UUID v4 injected upstream and echoed downstream, preserved from the client when present; runs first so even short-circuited responses carry it)response-compression(gzip / brotli / zstd for downstream clients, negotiated fromAccept-Encodingby Pingora's built-in compression module)hsts(Strict-Transport-Securityon direct HTTPS responses only, with optionalincludeSubDomainsandpreloaddirectives)proxy-cache(in-memory TTL response cache withx-cache/Ageheaders and a purge API). Only anonymous, unconditional GETs are eligible. Cookies, authorization, authenticated consumers, range/conditional requests, and request cache directives bypass it. Responses withSet-Cookie,Vary,Expires, or private/no-cache/no-store directives bypass it; upstream max-age/s-maxage and Age cap freshness. Cache content is limited to 64 MiB across 10,000 entries, isolated by route/service/HTTP-vs-HTTPS, and invalidated on reload.request-transform/response-transform(add / remove headers) andresponse-body-transform(find/replace on text bodies)http-log(batched JSON delivery of request records to an external collector, off the hot path)
- Auth:
-
WASM user plugins: upload
.wasmbinaries or WAT source and run them per route/service/globally, sandboxed and fuel-metered (wasmi). Modules implement a JSON-over-memory ABI (raahi_alloc,on_request,on_response) and can short-circuit requests, mutate headers, and react to upstream responses. Multiple wasm plugins stack on one route. Seeexamples/wasm/. -
Admin users, roles, and SSO: password sign-in (bcrypt) and OpenID Connect single sign-on (authorization code + PKCE; Google, Keycloak, Authentik, Pocket ID, Auth0, ...) with cookie sessions, plus
viewer/editor/adminroles enforced by the API. Optional auto-provisioning on first SSO login, restricted by email domain. The legacy admin bearer token (SHA-256, generated/rotated from the UI orPOST /api/v1/admin/token; SSE uses?access_token=) remains for automation and acts as admin. Auth is open until a user or token exists. Lockout recovery: empty theuserstable / clearsettings.admin_token_hashin SQLite and restart. -
TLS termination via boringssl with per-SNI certificate selection and live (no-restart) certificate reload, automatic ACME issuance and renewal via TLS-ALPN-01 or Cloudflare DNS-01 (including wildcards), and upstream TLS. Choose Let’s Encrypt (default), ZeroSSL (with EAB registration), or a custom ACME provider in the certificate form.
-
Health: active checks per target (TCP connect, or HTTP GET on a per-service
health_path, using HTTPS with certificate verification for HTTPS services, with 2xx/3xx = pass) with consecutive-failure thresholds and up to 16 concurrent target probes, plus passive circuit breaking. A failed connection removes the backend immediately. Multi-address hosts (e.g.localhost→ ::1 + 127.0.0.1) are resolved at configuration load and refreshed every 30 seconds (retaining last-known addresses on DNS failure). Probes try every address and elect the working one, which the proxy, L4 splicer, and health checks all share (GET /api/v1/healthshows it). -
Declarative config:
GET /api/v1/export(optionally with secrets for a restorable backup) andPOST /api/v1/import. Import replaces the configuration in one transaction and remaps IDs, usable for GitOps and disaster recovery. -
Prometheus:
GET /metricsexposition endpoint (requests, status classes, latency percentiles, per-route/consumer counters, target health gauges). -
JWKS / identity-provider auth: the
jwtplugin can verify RS256 tokens against ajwks_url(refreshed every 30s, kid-matched) instead of per-consumer credentials. This verifies tokens from Auth0, Keycloak, Google, and compatible issuers. -
Live observability: request metrics with latency percentiles (p50/p95/p99, µs precision), status breakdown, an SSE-driven dashboard with a "transit map" visualizing traffic flowing routes → services → targets (health-aware), a filterable live request log, and per-target health via
GET /api/v1/health. -
Operator tooling: a route tester (
GET /api/v1/router/test) that dry-runs the router for any method/host/path, and one-click config export (GET /api/v1/export, secrets excluded).
Workspace layout
| Crate | Responsibility |
|---|---|
raahi-core |
Domain types, the compiled ProxyConfig snapshot, and router matching (pure, unit-tested). |
raahi-store |
SQLite persistence (sqlx), migrations, CRUD, and snapshot building. |
raahi-proxy |
The Pingora ProxyHttp data plane: LB, plugins, health checks, metrics, hot reload. |
raahi-api |
axum admin REST API + UI serving (users, roles, sessions, OIDC SSO), run as a Pingora background service. |
raahi-acme |
ACME accounts, Cloudflare DNS-01, TLS-ALPN-01, issuance, and renewal. |
raahi (bin) |
Wires the store, data plane, and control plane into one Pingora server. |
ui/ |
Svelte 5 + Vite SPA (the "Aurora Transit" admin UI). |
Prerequisites
- Rust ≥ 1.84 (uses edition 2024).
- cmake, Go, Perl, and a C/C++ compiler are required to build Pingora's
native dependencies (BoringSSL via
boring-sys, andlibz-ng-sysfor compression). Go and Perl are build-time code generators only (BoringSSL generateserr_data.cand its assembly with them); nothing Go links into the binary. On most distros:apt install cmake golang perl/pacman -S cmake go perl. Without root,uv tool install cmake(orpip install cmake) provides cmake onPATH. Ifboring-sysfails with'stddef.h' file not found, libclang is installed without its builtin headers. Thejustrecipes detect this and setBINDGEN_EXTRA_CLANG_ARGSautomatically (just doctorshows the value); when calling cargo directly, exportBINDGEN_EXTRA_CLANG_ARGS="-I$(cc -print-file-name=include)"yourself. - Node ≥ 20 + pnpm (or npm) to build the UI.
just distrequires cargo-zigbuild + zig.uv tool install cargo-zigbuild(bundles zig via theziglangpackage; expose it aszigonPATH, e.g. a one-line shim runningpython -m ziglang).
Quick start
# 1. Build the UI (served by the admin API in production)
cd ui && pnpm install && pnpm build && cd ..
# 2. Build and run the proxy (creates raahi.db, seeds a demo service + route)
cargo run --release -- --seed
# Proxy: http://localhost:8080
# Admin UI: http://localhost:9080
Try it against two local upstreams:
python3 -m http.server 9001 &
python3 -m http.server 9002 &
curl localhost:8080/ # proxied + round-robined to :9001 / :9002
Release builds
just release # glibc build → target/release/raahi (dynamically linked to libc/libm)
just dist # fully static musl build → dist/raahi-v<version>-x86_64-unknown-linux-musl.tar.gz
just dist cross-compiles BoringSSL for musl with zig (cargo zigbuild), strips the binary,
and packages it with the built UI (ui/build, the binary's default --ui-dir) and this
README. The result runs on any x86_64 Linux with no shared-library dependencies:
tar xzf raahi-v*.tar.gz && cd raahi-v*/ && ./raahi.
Development
Run the backend, then the UI dev server (it proxies /api and SSE to the admin port):
cargo run -- --seed # backend on :8080 (proxy) and :9080 (admin)
cd ui && pnpm dev # UI with hot reload on http://localhost:5173
CLI / configuration
raahi [OPTIONS]
--db <URL> SQLite URL [env RAAHI_DB] (default sqlite://raahi.db)
--http-addr <ADDR> proxy HTTP listener override
--https-addr <ADDR> proxy HTTPS listener override
--admin-addr <ADDR> admin API listener override
--ui-dir <DIR> built UI to serve [env RAAHI_UI_DIR] (default ui/build)
--threads <N> proxy worker threads [env RAAHI_THREADS] (default all CPU cores)
--seed seed a demo service + route if the DB is empty
Fresh databases default to 0.0.0.0:8080 for HTTP, 0.0.0.0:8443 for HTTPS,
and 127.0.0.1:9080 for the admin API. CLI listener flags override persisted settings
for the process. Set log filtering with RUST_LOG (default info).
Listener addresses, the default LB algorithm, and the active TLS certificate live in the
settings table (editable in the UI). Routes, services, plugins, consumers, and
certificates are all managed via /api/v1/* and take effect immediately.
The complete documentation site lives in site/ and is built with Astro Starlight.
Run cd site && npm install && npm run dev for local documentation development.
Admin API (/api/v1)
The complete OpenAPI 3.0 contract is served at /openapi.yaml,
with an interactive reference at /docs. See
docs/API.md for authentication, common workflows, and operational caveats.
The API provides CRUD endpoints for services, services/{id}/targets, targets/{id}, routes,
plugins, consumers, consumers/{id}/credentials, credentials/{id}, certificates, and
settings. PUT replaces the complete resource rather than applying a partial update.
curl -X POST localhost:9080/api/v1/services \
-H 'content-type: application/json' \
-d '{"name":"api","protocol":"http"}'
TLS notes
Raahi terminates TLS with the boringssl backend, which supports dynamic per-SNI
certificate selection: all configured certificates are loaded into memory and a single
HTTPS listener serves the right one based on the SNI server name (exact or *.wildcard),
falling back to the active/default certificate for non-SNI or unmatched requests. Upstream
(proxy→backend) TLS is supported too.
Use the UI to upload PEM material, create an ACME-managed certificate, replace or remove a certificate, or choose the default certificate. The running HTTPS listener picks changes up live, with no restart, including the first certificate issued after startup.
ACME supports Let's Encrypt production/staging (the default), ZeroSSL, or a custom HTTPS directory. TLS-ALPN-01
requires the requested domains to resolve to Raahi with public port 443 reachable. DNS-01
uses a Cloudflare API token and is required for wildcard names. ZeroSSL uses DNS-01
in Raahi and requires an EAB key ID and base64url HMAC key from its Developer dashboard
for initial registration. Admins can save these in the certificate form; registered
accounts are reused per directory. EAB credentials stay out of certificate responses
and are included only in explicit secret exports. The background service issues
missing certificates immediately, retries failures, scans every six hours, and renews within
30 days of expiry. ACME account credentials, Cloudflare tokens, and certificate private keys
stay in SQLite and memory, are never logged or returned by ordinary APIs, and are included only
in exports made with include_secrets=true. Protect the database and secret exports accordingly.
Security notes
- Admin API authentication is off until configured. It defaults to loopback and is
open until a user or admin token exists. Then callers need a session cookie (password
or SSO sign-in;
HttpOnly,SameSite=Lax,Securebehind HTTPS) or the admin token (Authorization: Bearer .../X-Admin-Token). Roles gate what a session may do; failed password logins are rate limited per client IP. Keep network controls in place if the listener is exposed beyond localhost;/healthz,/metrics,/openapi.yaml,/docs,/api/v1/admin/status, and the login/SSO endpoints intentionally remain public. - User passwords are bcrypt-hashed; session tokens are stored as SHA-256 hashes; the SSO client secret lives in SQLite and is never returned by the API.
- Basic-auth passwords are hashed with bcrypt; Basic/JWT secrets and TLS private keys are not returned by ordinary resource APIs or written to logs. Key-auth API keys are identifiers and are returned by credential listings, so protect those responses.
- All database access uses parameterized queries (sqlx bind parameters).
- Defaults follow least privilege (no permissive CORS on the admin API; loopback admin bind).
Roadmap / out of scope
- gRPC and multi-node config sync.