Skip to content

arencloud/balor

Repository files navigation

Balor

Rust-native L4/L7 load balancer with an Axum admin API and a Yew (WASM) control panel.

Balor logo

Author: Eduard Gevorkyan (egevorky@arencloud.com)
License: Apache 2.0
Current version: UI 0.1.3 • API 0.1.3 • build dev

Architecture

flowchart LR
    subgraph ControlPlane["Control Plane (Axum)"]
        API["/api/* (CRUD + Auth/RBAC)"]
        Supervisor["Listener Supervisor"]
        Store["Config Store (file)"]
        Static["/assets + UI (ServeDir)"]
    end
    subgraph AdminUI["Admin UI (Yew/WASM)"]
        Form["Create/Edit Listener"]
        List["Active Listeners + Health"]
        Auth["Login + Users (Admin)"]
    end
    subgraph DataPlane["Data Plane"]
        HTTP["HTTP L7 Balancer\n(Axum proxy)"]
        TCP["TCP L4 Balancer\n(Tokio copy_bidirectional)"]
        Upstreams["Upstream Pools"]
    end

    AdminUI -->|XHR /api/listeners, /api/stats| API
    API --> Store
    API --> Supervisor
    Supervisor -->|Spawn| HTTP
    Supervisor -->|Spawn| TCP
    HTTP --> Upstreams
    TCP --> Upstreams
    AdminUI --> Static
Loading
  • Backend (backend): Axum HTTP server exposing CRUD APIs for listeners and serving the compiled admin UI. A supervisor spawns runtime tasks per listener:
    • HTTP (L7): Axum fallback proxy with round-robin upstream selection and header sanitization. Host-based routing is first-class: each host binds to an upstream pool and can carry its own TLS/ACME settings (SNI).
    • TCP (L4): Tokio copy_bidirectional bridge with round-robin upstream selection. TCP listeners now reuse upstream pools (pick a pool; the UI hydrates endpoints automatically).
  • Admin UI (admin): Yew single-page app compiled to WASM via Trunk. Provides listener creation, editing, deletion, and live stats against /api/*. Uses the logo at images/balor.png.
  • Workspace: Cargo workspace rooted at repository top; shared dependencies declared in the workspace [dependencies].

Run the control plane

Prerequisites: Rust toolchain, wasm32-unknown-unknown target, and trunk for the UI (cargo install trunk).

# Build the admin UI to admin/dist
cd admin
trunk build --release

# Run the backend (serves API + UI)
cargo run -p backend
# Admin UI available at http://127.0.0.1:8080

Environment knobs:

  • BALOR_HTTP_ADDR (default 0.0.0.0:9443) – admin API + UI bind address (port configurable; TLS for console planned).
  • BALOR_ADMIN_DIST (default admin/dist) – path where the Yew assets are served from.
  • BALOR_STATE_FILE (default data/balor_state.json) – persisted listener config storage.
  • BALOR_DEFAULT_ADMIN_PASSWORD (default admin) – bootstrap password for the auto-created admin user if no users exist.
  • BALOR_ADMIN_TOKEN (optional) – alternative bearer token that always maps to admin role (bypasses password).
  • BALOR_BOOTSTRAP_SECRET_FILE (default data/admin-bootstrap.txt) – when no users exist and no BALOR_DEFAULT_ADMIN_PASSWORD is set, a strong password is generated and written here (mode 600) for first login; rotate and delete after use.

Development / CI checks

  • make fmt – format workspace.
  • make backend / make admin-check – quick checks for backend/admin.
  • make ci – fmt --check, clippy (all targets/features), backend tests.
  • cargo run -p backend --bin balorctl -- --state data/balor_state.json export backup.json – CLI helper to export/import the persisted state file (see Makefile targets balor-export / balor-import).

Packaging

  • Docker: docker build -t balor:latest . (multi-stage, bundles admin/dist + backend). Run with -e BALOR_ADMIN_DIST=/app/admin/dist -p 9443:9443.
  • Homebrew (tap example under packaging/balor.rb): provides a formula to build UI + backend from source; set BALOR_ADMIN_DIST to the installed dist path (see caveats in the formula).
  • Backup/restore: use balorctl (cargo run -p backend --bin balorctl -- --state data/balor_state.json export OUT.json / ... import IN.json) or Makefile shortcuts make balor-export OUT=... / make balor-import IN=... to snapshot/restore the JSON state file.
  • Container image: make docker-build IMAGE=balor:0.1.3 REGISTRY=ghcr.io/your-org (tags optional). make docker-push will push both the registry tag and local tag when REGISTRY is set.
  • Homebrew checksum: v0.1.3 tarball SHA256 d5558cd419c8d46bdc958064cb97f963d1ea793866414c025906ec15033512ed is populated in packaging/balor.rb; update on next release tag.
  • GitHub Actions release workflow builds/tests, uploads artifacts, pushes GHCR images, and updates a Homebrew tap (expects ${OWNER}/homebrew-tap repo and GITHUB_TOKEN permissions).
  • Systemd: a sample unit (packaging/balor.service) and env file (packaging/balor.env.sample) are provided. Use make systemd-unit to stage copies in dist/, edit paths/passwords, then install to /etc/systemd/system/balor.service and /etc/balor/balor.env.

API sketch

  • GET /api/health – service heartbeat.
  • GET /api/stats – listener/task counts.
  • GET /api/listeners – list configured listeners.
  • POST /api/listeners – create listener. HTTP listeners rely on host-based routes: { name, listen, protocol: "http", rate_limit?, host_routes: [{ host, pool, rate_limit?, tls?, acme? }], upstreams: [...] } (TCP uses upstreams, typically hydrated from a selected pool in the UI). Pools are referenced by name. rate_limit is { rps, burst } and enforces 429 + Retry-After on overage.
  • GET /api/listeners/{id} – fetch a listener.
  • PUT /api/listeners/{id} – update a listener.
  • POST /api/listeners/{id}/drain – gracefully drain and disable a listener before delete/update (stops runtime, keeps config).
  • DELETE /api/listeners/{id} – remove a listener and stop its runtime.
  • GET/POST/DELETE /api/pools – manage reusable upstream pools (attach to host routes).
  • GET/POST/PUT/DELETE /api/acme/providers – manage DNS-01 provider profiles (Cloudflare, Route53, generic).
  • POST /api/acme/request – schedule an ACME issuance (host-bound or standalone) and persist the job; GET /api/acme/standalone, /acme/standalone/renew, /acme/standalone/delete manage standalone jobs.
  • GET /api/certificates – list uploaded/issued certs; POST uploads a PEM bundle.
  • POST /api/login / POST /api/logout – session tokens for the UI.
  • GET/POST/PUT/DELETE /api/users – RBAC user management (admin only).
  • GET/PUT /api/logging – get/set trace sampling (permyriad, min 1–max 10_000).

Notes

  • HTTP upstream addresses should include scheme (e.g., http://127.0.0.1:7000). TCP upstreams use host:port. HTTP host routes pick from pools (pool selection is required per host); TCP listeners select a pool and the UI fills endpoints for you. Weighted upstreams are supported by adding an optional weight after a space, e.g., api=http://10.0.0.2:8080 5 (defaults to 1).
  • State persists to data/balor_state.json on each change (path override via BALOR_STATE_FILE).
  • Background health checks run every ~5 seconds and mark upstreams up/down in the UI automatically; HTTPS probes verify certificates by default, with an opt-out toggle in the UI when you intentionally use self-signed upstreams. Data-plane proxying will still pass traffic to HTTPS upstreams with self-signed/cluster CAs (OpenShift APIs/routers) even if health validation is enabled.
  • HTTP listeners can terminate TLS via PEM cert/key paths; files are reloaded when they change. Multiple certificates on one port are supported via per-host SNI selection; a listener-level certificate acts as fallback.
  • Sticky sessions supported per HTTP listener (cookie or client IP hash).
  • Upstream Set-Cookie domains are rewritten to the client-facing host to keep sessions alive across refreshes when proxied.
  • RBAC roles: Admin (full), Operator (CRUD listeners), Viewer (read-only).
  • Prometheus metrics exposed at /metrics (HTTP counters/latency and TCP connection totals per listener).
  • WebSocket pass-through supported for HTTP listeners (upgrade + bidirectional frames).
  • ACME automation: HTTP-01 responder serves tokens from BALOR_ACME_CHALLENGE_DIR (default data/acme-challenges); HTTP-01 issuance now runs automatically for host routes that request ACME and stores PEMs under data/certs. DNS-01 flow uses provider profiles (Cloudflare, Route53, generic); Cloudflare automation shipped, Route53/generic automation still pending. Standalone ACME jobs persist with validity dates and can be renewed from the UI.
  • Certificates dashboard: upload/download manual PEM bundles (stored under BALOR_CERT_DIR, default data/certs) from the Admin UI Certificates tab.
  • ACME provider type auto-sets API base URLs (Cloudflare/Route53) with overrides allowed.
  • The UI defaults to a sample listen address (0.0.0.0:9000) and a single upstream; adjust per environment.
  • Browser support note: Chrome/Chromium and Firefox are both supported; Users/Metrics views were refactored to avoid the previous Firefox layout crash.

OpenShift quick setup

  • API server (6443): Create a TCP listener on 0.0.0.0:6443 pointing at a pool of control-plane nodes on https://<node>:6443. Health probes can be TCP (script) or HTTPS with “skip TLS verify” enabled if using cluster CAs.
  • Routers (80/443): Create HTTP listeners on :80 and :443, attach host-based routes to router pools, and enable SNI/TLS per host. WebSockets/HTTP2 are passed through; self-signed router certs are accepted automatically at the proxy layer while health checks still verify unless disabled. For API/routers addressed by IP, Balor now preserves the original Host header and forces SNI to the client host while dialing the IP via DNS override to satisfy cluster-issued certs.

Feature status (near-term)

  • ✅ Persistent config store
  • ✅ Health checks (HTTP/TCP)
  • ✅ TLS termination (hot reload, per-host SNI)
  • ✅ Sticky sessions (cookie/IP hash)
  • ✅ RBAC (admin/operator/viewer)
  • ✅ Metrics (Prometheus HTTP/TCP)
  • ✅ Cookie rewrite to client host
  • ✅ Host-based routing & SNI multi-cert
  • ✅ Pools dashboard; listeners/pools reuse
  • ✅ WebSocket pass-through
  • ✅ Certificates dashboard (manual PEM upload/download; ACME-issued certs register)
  • ✅ Version banner: UI/API/build version surfaced via /api/version and shown in the header
  • ✅ Metrics tab now includes latency quantiles (p50/p95/p99) derived from Prometheus buckets with quick visual cards
  • ✅ Logs tab (admin-only): live buffer view, level filter, JSONL download/preview from the backend log store (retention + rotation), and hardened sampling controls (clamped 1–100% with safe persistence)
  • ✅ Admin console port/TLS controls: default bind 0.0.0.0:9443, UI control to change port and pick a console cert (restart required)
  • ✅ Rate limiting: per-listener and per-host-route token-bucket limits (RPS + burst) with 429 + Retry-After on overage
  • ✅ ACME automation: HTTP-01 + Cloudflare/Route53/Generic (webhook) DNS-01 with periodic renewal and backoff retries. ACME jobs (host-bound & standalone) persist with “valid until” dates; renew/edit/remove from UI; existing ACME certs are reused on edit to avoid needless re-issuance. Standalone issuance is timeout-guarded to prevent console hangs.
  • ✅ Listener bind flexibility: any valid host:port accepted in the UI/API for HTTP/TCP listeners; backend enforces socket-addr validity only.
  • ✅ gRPC pass-through: HTTP listeners preserve TE: trailers; point upstreams at your gRPC service.
  • ✅ Health probes: configurable HTTP health path/headers and optional probe scripts with timeouts/env to mark upstream health.
  • ✅ Browser support: Chrome/Chromium and Firefox verified after Users/Metrics fixes

About

Balor is a minimal, high-performance load balancer built in Rust.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages