Skip to content

solisoft/soli-proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

321 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Soli Proxy

A high-performance, production-ready forward proxy server built in Rust with HTTP/2+ support, automatic HTTPS, and hot config reload.

Features

  • HTTP/2+ Support: Native HTTP/2 with automatic fallback to HTTP/1.1
  • Automatic HTTPS: Self-signed certificates for development, Let's Encrypt for production
  • Hot Config Reload: Update configuration without dropping connections
  • Simple Configuration: Custom config format with comments support
  • Load Balancing: Round-robin, weighted, and health-checked backends
  • WebSocket Support: Full WebSocket proxy capabilities
  • Middleware: Authentication (Basic, API Key, JWT), Rate Limiting, JSON Logging
  • Health Checks: Kubernetes-compatible liveness and readiness probes
  • App Health Monitoring: Automatic health checks with auto-restart for managed apps
  • High Performance: Built on Tokio and Hyper for maximum throughput

Quick Start

Development Mode

# Build and run in dev mode
cargo run --bin soli-proxy -- --dev

# With custom config and sites directory
cargo run --bin soli-proxy -- --conf ./my-proxy.conf --sites-dir ./my-sites

Production Mode

# Build release
cargo build --release

# Run in production mode (requires Let's Encrypt config)
./target/release/soli-proxy

# Run as daemon
./target/release/soli-proxy -d

# With custom paths
./target/release/soli-proxy -c /etc/proxy.conf --sites-dir /var/sites

CLI Options

soli-proxy [OPTIONS] [COMMAND]

Options:
  -c, --conf <CONF>            Config file [default: ./proxy.conf]
  -d, --daemon                 Run as daemon
      --dev                    Development mode
      --watch <WATCH>          Watch config & sites for changes [default: true]
      --sites-dir <SITES_DIR>  Sites directory [default: ./sites]
  -h, --help                   Print help
  -V, --version                Print version

Subcommands

App lifecycle commands operate on apps discovered in --sites-dir and can be run while the proxy is running (they read shared state from ./run):

soli-proxy deploy  [-c <conf>] <app_name>   # Blue-green deploy: build & switch to the other slot
soli-proxy restart [-c <conf>] <app_name>   # Restart the currently active slot
soli-proxy stop    [-c <conf>] <app_name>   # Stop the app
soli-proxy logs    [-c <conf>] <app_name>   # Print deployment logs for both slots

Other subcommands:

soli-proxy tui [-c <conf>] [--sites-dir <DIR>] [--dev]   # Interactive terminal UI
soli-proxy update [--reinstall]                          # Self-update from GitHub releases

Configuration

Main Config (config.toml)

[server]
bind = "0.0.0.0:8080"
https_port = 8443
worker_threads = "auto"

[tls]
mode = "auto"  # "auto" for dev, "letsencrypt" for production

[letsencrypt]
email = "admin@example.com"
staging = false

[logging]
level = "info"
format = "json"
log_endpoints = true  # log one line per request (method, path, host, status, latency)

[metrics]
enabled = true
endpoint = "/metrics"

[health]
enabled = true
liveness_path = "/health/live"
readiness_path = "/health/ready"

[rate_limiting]
enabled = true
requests_per_second = 1000
burst_size = 2000

TLS Certificates

The proxy stores certificates flat in tls.cache_dir (default ./certs).

Filename pattern What it is Example
<domain>.cert.pem + <domain>.key.pem Per-domain cert. Matches SNI exactly. The cert MUST list <domain> in its SANs. crm.example.com.cert.pem
_wildcard.<parent>.cert.pem + _wildcard.<parent>.key.pem Wildcard cert covering *.<parent>. Matches one label deep per RFC 6125. The cert MUST list *.<parent> in its SANs. _wildcard.example.com.cert.pem covers crm.example.com, api.example.com, etc.
self-signed.cert.pem + self-signed.key.pem Reserved fallback name. Used when no per-domain or wildcard match. Don't use for a real domain. (auto-generated)

Resolution order on a TLS handshake: exact-match certs/<sni>.cert.pem β†’ wildcard certs/_wildcard.<parent>.cert.pem (one label deep) β†’ self-signed fallback. Cert files are scanned once at startup β€” SIGUSR1 and the admin reload endpoint only refresh routing, so adding/replacing a cert file requires a full proxy restart.

For local dev with mkcert β€” install the local CA on your machine (mkcert -install) and drop wildcard certs in:

mkcert "*.example.test"
mv _wildcard.example.test.pem      ./certs/_wildcard.example.test.cert.pem
mv _wildcard.example.test-key.pem  ./certs/_wildcard.example.test.key.pem

After restart, every *.example.test alias is served with a Mac/Linux-trusted cert (no browser warning).

See docs/tls-mkcert.md for the full mkcert workflow, the CA-rotation pitfall (identical issuer string, different key β†’ bad signature), and the scripts/diag-mkcert-mac.sh / scripts/regen-mkcert-and-deploy.sh helpers.

Proxy Rules (proxy.conf)

# Comments are supported
default -> http://localhost:3000

/api/* -> http://localhost:8080
/ws -> ws://localhost:9000

# Load balancing
/api/* -> http://10.0.0.10:8080, http://10.0.0.11:8080, http://10.0.0.12:8080

# Weighted routing
/api/heavy -> weight:70 http://heavy:8080, weight:30 http://light:8080

# Regex routing
~^/users/(\d+)$ -> http://user-service:8080/users/$1

# External https backend (Host/Origin are rewritten to the target's own
# authority; the client's host is forwarded via X-Forwarded-Host)
mirror.example.com -> https://origin.example.net

# Permanent redirect to a new canonical domain (301, path and query preserved)
old.example.com -> redirect://new.example.com

# Headers to add
headers {
    X-Forwarded-For: $client_ip
    X-Forwarded-Proto: $scheme
}

# Authentication
/auth/* {
    auth: basic
    realm: "Restricted"
}

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Soli Proxy Server                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ Config      β”‚  β”‚ TLS/HTTPS   β”‚  β”‚ HTTP/2+      β”‚ β”‚
β”‚  β”‚ Manager     β”‚  β”‚ Handler     β”‚  β”‚ Listener     β”‚ β”‚
β”‚  β”‚ (hot reload)β”‚  β”‚ (rcgen/LE)  β”‚  β”‚ (tokio/hyper)β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚         β”‚                β”‚               β”‚          β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚                          β”‚                          β”‚
β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚                   β”‚   Router    β”‚                   β”‚
β”‚                   β”‚ (matching)  β”‚                   β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                          β”‚                          β”‚
β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚         β”‚                β”‚                β”‚         β”‚
β”‚    β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”    β”‚
β”‚    β”‚ Auth    β”‚     β”‚ Rate      β”‚     β”‚ Logging β”‚    β”‚
β”‚    β”‚ Middle  β”‚     β”‚ Limit     β”‚     β”‚ JSON    β”‚    β”‚
β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Command Line Options

soli-proxy [dev|prod] [OPTIONS]

Modes:
  dev   Development mode with self-signed certificates
  prod  Production mode with Let's Encrypt support

Environment Variables:
  SOLI_CONFIG_PATH    Path to proxy.conf (default: ./proxy.conf)

Project Structure

soli-proxy/
β”œβ”€β”€ Cargo.toml
β”œβ”€β”€ config.toml           # Main configuration
β”œβ”€β”€ proxy.conf            # Proxy rules
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main.rs           # Entry point
β”‚   β”œβ”€β”€ lib.rs            # Library root
β”‚   β”œβ”€β”€ bin/
β”‚   β”‚   β”œβ”€β”€ httptest.rs   # End-to-end proxy throughput test
β”‚   β”‚   └── hash-password.rs
β”‚   β”œβ”€β”€ config/           # Config parsing & hot reload
β”‚   β”œβ”€β”€ server/           # HTTP/HTTPS server
β”‚   β”œβ”€β”€ admin/            # Admin API server
β”‚   β”œβ”€β”€ acme/             # ACME / Let's Encrypt
β”‚   β”œβ”€β”€ tls.rs            # TLS & certificate management
β”‚   β”œβ”€β”€ circuit_breaker.rs
β”‚   β”œβ”€β”€ metrics.rs        # Prometheus-format metrics
β”‚   β”œβ”€β”€ pool.rs           # Connection pool
β”‚   β”œβ”€β”€ auth.rs           # Authentication
β”‚   β”œβ”€β”€ app/              # App management & blue-green deploy
β”‚   └── shutdown.rs       # Graceful shutdown
β”œβ”€β”€ benches/
β”‚   β”œβ”€β”€ routing.rs        # Rule matching & scaling benchmarks
β”‚   β”œβ”€β”€ components.rs     # Circuit breaker, load balancer, metrics
β”‚   └── config_parsing.rs # Config file parsing benchmarks
└── scripts/              # Helper scripts

Performance

Built on Tokio and Hyper with SO_REUSEPORT multi-listener architecture.

End-to-End Throughput (50k requests, 200 concurrent)

Endpoint Throughput p50 p95 p99
Proxy (default route β†’ backend) 228,196 req/s 0.64 ms 0.92 ms 1.20 ms
Admin API (GET /api/v1/status) 508,049 req/s 0.37 ms 0.58 ms 0.71 ms

Micro-benchmarks (criterion)

Component Operation Time
Routing Domain match 54 ns
Routing Regex match 57 ns
Routing 500 rules worst-case 587 ns
Circuit breaker is_available (1k targets) 18 ns
Load balancer select_index (round-robin) 1.6 ns
Metrics record_request 29 ns
Metrics format_metrics (1k requests) 601 ns
Config parsing 5 rules 6.9 Β΅s
Config parsing 100 rules 45 Β΅s

Running benchmarks

# Criterion micro-benchmarks (routing, components, config parsing)
cargo bench

# End-to-end proxy throughput test
cargo run --release --bin httptest -- --requests 50000 --concurrency 200

Hot Reload

Configuration changes are detected automatically:

  1. File watcher monitors proxy.conf
  2. On change, config is reloaded atomically
  3. New connections use new config
  4. Existing connections continue with old config
  5. Graceful draining of old connections

App Configuration (app.infos)

When apps are managed by the proxy (via the sites directory, e.g. ./www), each app directory must be named after its domain (must contain at least one dot, e.g. myapp.example.org/) and may contain an app.infos file describing how to run it.

app.infos is a flat TOML file (no section headers β€” keys live at the top level). The file is optional; if missing or empty, defaults are used.

Example

# www/proxy.solisoft.net/app.infos
name = "proxy.solisoft.net"
domain = "proxy.solisoft.net"
start_script = "soli serve . --port $PORT --workers $WORKERS"
workers = 2
stop_script = ""
health_check = "/health"
graceful_timeout = 30
port_range_start = 20000
port_range_end = 30000

Fields

Field Type Default Description
name string directory name Logical app name (used in logs, admin API).
domain string directory name (when auto-detected) Domain the app serves. Matched against the Host header.
start_script string auto-detected (see below) Command used to launch the app. Supports $PORT and $WORKERS substitution. Parsed without a shell β€” no pipes/redirects/globs.
stop_script string none Optional command to run when stopping the app.
health_check string "/health" (or "/" when auto-detected) HTTP path the proxy polls every 30s to decide if the app is alive.
graceful_timeout int (seconds) 30 Time given to the old process to exit cleanly during a blue/green swap.
drain_delay int (seconds) 5 Time to keep the old process draining existing connections before shutdown. Clamped to < graceful_timeout (set to graceful_timeout / 2 if too large).
port_range_start int 20000 Lower bound of the port range used to allocate blue/green slots.
port_range_end int 30000 Upper bound of the port range.
workers int 1 Number of worker processes the app should spawn. Exposed as $WORKERS in start_script and as the WORKERS env var.
user string [apps].default_user from config.toml OS user to drop privileges to (required when running the proxy as root).
group string [apps].default_group from config.toml OS group to drop privileges to.
docker_image string none If set, the app runs inside Docker using this image instead of a host process.
docker_options string none Extra flags appended to docker run (validated against an allowlist).
docker_network string "soli-apps" Docker network the container joins (created automatically if missing).

Variable substitution

start_script supports two placeholders, replaced before the process is launched:

  • $PORT β€” the slot port allocated by the proxy (from port_range_start..port_range_end).
  • $WORKERS β€” the value of the workers field.

The same values are also exported as environment variables (PORT, WORKERS, and HEALTH_CHECK for Docker), so scripts that don't use $VAR substitution can still read them from the environment.

Auto-detection

If start_script is omitted, the proxy tries to infer one from the app directory:

  • Soli app β€” when app/ and app/models/ exist:
    • start_script β†’ soli serve . --port $PORT --workers $WORKERS (with --dev appended in dev mode)
    • health_check β†’ /
  • LuaOnBeans app β€” when a luaonbeans.org binary exists in the directory:
    • start_script β†’ ./luaonbeans.org -D . -p $PORT -s
    • health_check β†’ /

If no start_script is set and neither layout is detected, deployment fails with No start script configured.

App Health Monitoring

When apps are managed by the proxy, it automatically:

  • Polls each app's health_check path every 30 seconds
  • Auto-restarts any app that fails (connection refused, timeout, etc.)
  • Only restarts on actual failures, not on non-2xx responses

See App Configuration above for how to set health_check per app.

Systemd Service

Install soli-proxy as a systemd service for automatic restart on failure:

# Copy the service file
sudo cp scripts/soli-proxy.service /etc/systemd/system/

# Reload systemd
sudo systemctl daemon-reload

# Enable and start
sudo systemctl enable soli-proxy
sudo systemctl start soli-proxy

# Check status
sudo systemctl status soli-proxy

# View logs
journalctl -u soli-proxy -f

The service file is located at scripts/soli-proxy.service.

Commit messages

This project uses Conventional Commits for semantic release. Use the format type(scope): description (e.g. feat(proxy): add retry). Allowed types: feat, fix, docs, style, refactor, perf, test, chore, ci, build.

Optional setup:

  • Commit template (reminder in the message box): git config commit.template .gitmessage
  • Auto-fix non-conventional messages (prepend chore: if the first line doesn’t match): cp scripts/git-hooks/prepare-commit-msg .git/hooks/prepare-commit-msg && chmod +x .git/hooks/prepare-commit-msg

License

MIT

About

Simple but powerful proxy server

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors