A high-performance, configurable virtual host proxy built on Cloudflare's Pingora proxy framework. Features dynamic backend management, health checking, metrics, and support for A/B testing through tag-based backend switching.
- Virtual Host Routing: Route multiple domains to different backend pools based on SNI
- Dynamic Configuration: Runtime configuration via RESTful API - no restarts required
- Health Checking: Automatic backend health monitoring with configurable thresholds
- A/B Testing: Switch between backend pools on-the-fly for canary deployments
- Metrics & Observability: Prometheus-compatible metrics endpoint
- TLS Termination: Full HTTPS support with SNI-based certificate selection
- Dual Certificate Sources: File-based certificates (nginx-style) and Let's Encrypt automatic certificates
- HTTP to HTTPS Redirect: Optional automatic redirect for enhanced security
- Hot Certificate Reload: Update certificates without downtime
- Certificate Management API: Monitor and reload certificates via RESTful API
- Automatic Certificate Renewal: Let's Encrypt certificates auto-renew before expiration
- Full Certificate Chain: Proper certificate chain loading for complete TLS validation
- Rust 1.70+ (for building from source)
- Linux/BSD/macOS (Pingora currently doesn't support Windows)
- Clone the repository:
git clone [Replace with your repository URL]
cd front_tier- Build the project:
cargo build --release- Create a configuration file:
cp config.example.toml config.toml
# Edit config.toml with your settings- Run the proxy:
./target/release/pingora-vhostThe proxy will start on port 443 (HTTPS) and 80 (HTTP), with the management API on port 8080.
The proxy uses a TOML configuration file. Here's a minimal example:
[proxy]
listen_addr = "0.0.0.0:443" # HTTPS port
listen_addr_http = "0.0.0.0:80" # HTTP port
management_api_addr = "127.0.0.1:8080" # API port
[logging]
level = "info"
format = "text"
[metrics]
enabled = true
listen_addr = "0.0.0.0:9090" # Prometheus metrics
[health_check]
interval_secs = 10
timeout_secs = 5
unhealthy_threshold = 3
healthy_threshold = 2
# Define your virtual hosts (domains)
[[virtual_hosts]]
domain = "example.com"
enabled_backends_tag = "a" # Use backends tagged "a"
http_to_https = true
# Define backend servers
[[backends]]
id = "web-v1"
address = "localhost:3001"
tags = ["a"]
[[backends]]
id = "web-v2"
address = "localhost:3002"
tags = ["b"]listen_addr: Address/port for HTTPS connectionslisten_addr_http: Address/port for HTTP connectionsmanagement_api_addr: Address/port for the management API
email: Email for Let's Encrypt account (required for automatic certificates)staging: Use Let's Encrypt staging environment (recommended for testing)cache_dir: Directory to cache certificatesrenewal_check_interval_secs: How often to check for certificate renewal (default: 86400 = daily)renewal_days_before_expiry: Days before expiration to renew certificates (default: 30)dns_propagation_secs: Seconds to wait for DNS propagation when using DNS-01 (default: 30)
level: Log level (trace, debug, info, warn, error)format: Log format (text or json)output: Output destination (console or file)file_path: Path to log file (when output=file)
enabled: Enable/disable metrics collectionlisten_addr: Address for Prometheus metrics endpoint
interval_secs: Health check interval in secondstimeout_secs: Health check timeout in secondsunhealthy_threshold: Number of failed checks before marking unhealthyhealthy_threshold: Number of successful checks before marking healthy
domain: Domain name to routeenabled_backends_tag: Tag of backends to route traffic tohttp_to_https: Redirect HTTP to HTTPStls_enabled: Enable TLS for this domain (default: true)certificate_source: Certificate configuration (optional)type: Either "file" or "lets_encrypt"- For "file" type:
cert_path: Path to certificate file (.crt or .pem)key_path: Path to private key file (.key)
- For "lets_encrypt" type: No additional fields needed
id: Unique backend identifieraddress: Backend address (host:port)tags: List of tags for grouping backends
The proxy supports two certificate sources:
Use existing certificates in standard formats:
[[virtual_hosts]]
domain = "secure.example.com"
enabled_backends_tag = "a"
http_to_https = true
tls_enabled = true
[virtual_hosts.certificate_source]
type = "file"
cert_path = "/etc/ssl/certs/secure.example.com.crt"
key_path = "/etc/ssl/private/secure.example.com.key"Supported formats:
- Certificates:
.crt,.pem,.cer - Private keys:
.key,.pem(unencrypted or PKCS#8)
Automatic certificate issuance and renewal:
[[virtual_hosts]]
domain = "auto-secure.example.com"
enabled_backends_tag = "a"
http_to_https = true
tls_enabled = true
[virtual_hosts.certificate_source]
type = "lets_encrypt"Requirements:
- Domain DNS must point to the proxy server
- Port 80 must be accessible for HTTP-01 challenge
- Configure
[lets_encrypt]section with your email
Let's Encrypt Configuration:
[lets_encrypt]
email = "admin@example.com"
staging = true # Use staging for testing, set false for production
cache_dir = "/etc/pingora-ssl/certs"
renewal_check_interval_secs = 86400 # Check daily (optional)
renewal_days_before_expiry = 30 # Renew 30 days before expiry (optional)Automatic Certificate Renewal:
Let's Encrypt certificates are automatically renewed before expiration:
- Background task checks certificate expiration periodically (default: daily)
- Certificates are renewed when within the renewal window (default: 30 days before expiry)
- Renewed certificates are automatically reloaded without service interruption
- Only Let's Encrypt certificates are auto-renewed (file-based certificates show warnings)
Example: Renew 1 day before expiration:
[lets_encrypt]
email = "admin@example.com"
staging = false
cache_dir = "/etc/pingora-ssl/certs"
renewal_check_interval_secs = 86400 # Check every 24 hours
renewal_days_before_expiry = 1 # Renew 1 day before expiryIf your server is in China or port 80 is blocked by ICP/ISP firewalls, use DNS-01 challenge validation:
[lets_encrypt]
email = "admin@example.com"
staging = true
cache_dir = "./acme-certs"
[lets_encrypt.dns_provider]
provider = "aliyun"
access_key_id = "your-access-key-id"
access_key_secret = "your-access-key-secret"
dns_propagation_secs = 30Supported DNS providers:
- Aliyun (Alibaba Cloud) DNS
- Cloudflare DNS
- DNSPod (Tencent Cloud) DNS
DNS-01 vs HTTP-01:
- DNS-01 works when port 80 is blocked
- DNS-01 supports wildcard certificates
- HTTP-01 is simpler but requires public HTTP access
Automatically redirect HTTP traffic to HTTPS:
[[virtual_hosts]]
domain = "example.com"
http_to_https = true # Redirects all HTTP traffic to HTTPSWhen enabled:
- HTTP requests to port 80 receive 301 Permanent Redirect
- Clients are redirected to the same URL with https://
- SNI routing works seamlessly after redirect
The proxy automatically selects the correct certificate based on the TLS Server Name Indication (SNI) extension:
# Multiple domains with different certificates
[[virtual_hosts]]
domain = "example.com"
enabled_backends_tag = "a"
[virtual_hosts.certificate_source]
type = "file"
cert_path = "/etc/ssl/certs/example.com.crt"
key_path = "/etc/ssl/private/example.com.key"
[[virtual_hosts]]
domain = "api.example.com"
enabled_backends_tag = "api"
[virtual_hosts.certificate_source]
type = "lets_encrypt"
[[virtual_hosts]]
domain = "blog.example.com"
enabled_backends_tag = "blog"
[virtual_hosts.certificate_source]
type = "file"
cert_path = "/etc/ssl/certs/blog.com.crt"
key_path = "/etc/ssl/private/blog.com.key"Each domain presents its own certificate during TLS handshake.
Let's Encrypt certificates include the complete certificate chain:
- End-entity certificate for your domain
- Intermediate certificate from Let's Encrypt
- Root certificate (implicitly trusted)
This ensures proper TLS validation across all clients:
# Verify certificate chain is complete
openssl s_client -connect example.com:443 -servername example.com
# Expected output depth:
# depth=2: ISRG Root X1 (root)
# depth=1: Let's Encrypt E8 (intermediate)
# depth=0: example.com (end-entity)
# Verify return code: 0 (ok)The proxy automatically loads and serves the full certificate chain during TLS handshake, ensuring:
- Complete certificate validation
- Compatibility with all browsers and clients
- No security warnings about incomplete certificate chains
The management API runs on the configured management_api_addr (default: http://127.0.0.1:8080).
GET /api/v1/healthReturns "OK" if the service is healthy.
GET /api/v1/domainsResponse:
[
{
"domain": "example.com",
"enabled_backends_tag": "a",
"http_to_https": true
}
]GET /api/v1/domains/:domainPOST /api/v1/domains
Content-Type: application/json
{
"domain": "example.com",
"enabled_backends_tag": "a",
"http_to_https": true
}PUT /api/v1/domains/:domain
Content-Type: application/json
{
"enabled_backends_tag": "b",
"http_to_https": false
}DELETE /api/v1/domains/:domainGET /api/v1/backendsResponse:
[
{
"id": "web-v1",
"address": "localhost:3001",
"tags": ["a"]
}
]GET /api/v1/backends/:idPOST /api/v1/backends
Content-Type: application/json
{
"id": "web-v2",
"address": "localhost:3002",
"tags": ["b"]
}PUT /api/v1/backends/:id
Content-Type: application/json
{
"address": "localhost:3003",
"tags": ["a", "b"]
}DELETE /api/v1/backends/:idGET /api/v1/certificatesResponse:
[
{
"domain": "example.com",
"expires_at": "2025-04-15T12:00:00Z",
"days_until_expiration": 89,
"source": "file"
},
{
"domain": "api.example.com",
"expires_at": "2025-03-20T08:30:00Z",
"days_until_expiration": 63,
"source": "lets_encrypt"
}
]POST /api/v1/certificates/:domain/reloadManually reload a certificate from disk (useful after updating certificate files):
Response:
{
"message": "Certificate reloaded successfully for domain: example.com",
"domain": "example.com"
}Note: Certificates are automatically reloaded when files change. This endpoint is for manual reloads if needed.
GET /api/v1/metricsReturns Prometheus-formatted metrics.
The proxy supports A/B testing and canary deployments through tag-based backend switching. Here's how it works:
- Backend Tags: Each backend can have multiple tags (e.g., "a", "b", "stable", "canary")
- Domain Configuration: Each domain is configured to use one tag at a time
- Runtime Switching: Switch the active tag for a domain without restarting the proxy
# Production backends (tagged "a")
[[backends]]
id = "app-v1-stable"
address = "backend-prod-1:8080"
tags = ["a", "stable"]
[[backends]]
id = "app-v1-stable-2"
address = "backend-prod-2:8080"
tags = ["a", "stable"]
# Canary backends (tagged "b")
[[backends]]
id = "app-v2-canary"
address = "backend-canary:8080"
tags = ["b", "canary"]
# Domain configured to use stable backends
[[virtual_hosts]]
domain = "app.example.com"
enabled_backends_tag = "a"
http_to_https = true- Start with all traffic on stable (tag "a")
- Switch to canary (tag "b"):
curl -X POST http://127.0.0.1:8080/api/v1/domains/app.example.com/switch \
-H "Content-Type: application/json" \
-d '{"new_tag": "b"}'- Monitor metrics at
http://127.0.0.1:8080/api/v1/metrics - Rollback if needed by switching back to tag "a":
curl -X POST http://127.0.0.1:8080/api/v1/domains/app.example.com/switch \
-H "Content-Type: application/json" \
-d '{"new_tag": "a"}'For gradual rollout, you can configure multiple domains with different backend weights:
# 10% of users to canary
[[virtual_hosts]]
domain = "canary.app.example.com"
enabled_backends_tag = "b"
# 90% of users to stable
[[virtual_hosts]]
domain = "app.example.com"
enabled_backends_tag = "a"Then use your DNS or load balancer to route 10% of traffic to the canary subdomain.
The proxy exposes Prometheus-compatible metrics at /api/v1/metrics on the management API server (default port: 8080). Available metrics include:
proxy_requests_total: Total number of proxied requestsproxy_latency_seconds: Request latency histogramhealth_check_status: Backend health check statusapi_requests_total: API request count by endpointapi_latency_seconds: API request latency
curl http://127.0.0.1:8080/api/v1/metricsAdd to your prometheus.yml:
scrape_configs:
- job_name: 'pingora-proxy'
static_configs:
- targets: ['proxy-server:8080']
metrics_path: '/api/v1/metrics'# Debug build
cargo build
# Release build
cargo build --release# Run all tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Run specific test
cargo test test_health_checksrc/
├── main.rs # Entry point
├── config.rs # Configuration loading and validation
├── state.rs # Application state management
├── proxy.rs # Pingora proxy integration (TODO)
├── backend_pool.rs # Backend selection logic
├── health_check.rs # Health checking implementation
├── api/ # Management API
│ ├── server.rs # API server setup
│ ├── domains.rs # Domain endpoints
│ └── backends.rs # Backend endpoints
└── observability/ # Logging and metrics
├── logging.rs
└── metrics.rs
Problem: Proxy fails to start with "certificate not found" error
Solutions:
- Verify certificate file paths are correct
- Check file permissions (proxy needs read access)
- Ensure certificate and key match:
# Compare certificate modulus openssl x509 -noout -modulus -in /path/to/cert.crt | openssl md5 openssl rsa -noout -modulus -in /path/to/key.key | openssl md5 # Both should output the same hash
Problem: Wrong certificate presented for a domain
Solutions:
- Verify domain configuration matches the requested hostname
- Check DNS resolves to correct IP
- Test SNI manually:
openssl s_client -connect example.com:443 -servername example.com # Verify the certificate presented
Problem: Automatic certificate issuance fails
Solutions:
- Verify DNS points to proxy server
- Check port 80 is accessible from internet:
# From external machine curl http://your-domain.com/.well-known/acme-challenge/test - Check Let's Encrypt rate limits:
- Staging: No strict limits
- Production: 50 certificates per domain per week
- Review proxy logs for ACME challenge errors
- Test with staging first (staging = true)
Monitor certificates via API:
# Check all certificates
curl http://127.0.0.1:8080/api/v1/certificates
# Set up automated monitoring
watch -n 3600 'curl -s http://127.0.0.1:8080/api/v1/certificates | jq ".[] | select(.days_until_expiration < 30)"'Problem: Certificate changes not taking effect
Solutions:
- Verify file watcher is running (check logs)
- Manually reload via API:
curl -X POST http://127.0.0.1:8080/api/v1/certificates/example.com/reload
- Check file system events:
# Linux: install inotify-tools inotifywait -m /etc/ssl/certs/
Problem: Certificates not renewing automatically
Solutions:
- Check renewal manager is running in logs:
Starting automatic certificate renewal (check every 86400s, renew 1 days before expiry) - Verify Let's Encrypt configuration includes
certificate_source = { type = "lets_encrypt" } - Check certificate expiration via API:
curl http://127.0.0.1:8080/api/v1/certificates
- Manual renewal test (will renew if within window):
# Trigger immediate check by restarting service systemctl restart pingora-vhost - Review logs for ACME/DNS errors:
journalctl -u pingora-vhost -f | grep -i renewal
Problem: Certificate expired before renewal
Solutions:
- Renewal window might be too short - increase
renewal_days_before_expiry - Check renewal interval - ensure
renewal_check_interval_secsis reasonable (default: daily) - Verify service has been running continuously (not restarted during renewal window)
Example configuration for aggressive renewal:
[lets_encrypt]
email = "admin@example.com"
staging = false
cache_dir = "/etc/pingora-ssl/certs"
renewal_check_interval_secs = 43200 # Check every 12 hours
renewal_days_before_expiry = 7 # Renew 7 days before expiryProblem: Backends marked as unhealthy
Solutions:
- Test health endpoint manually:
curl http://backend-ip:port/health
- Adjust health check thresholds in config
- Check network connectivity
- Review backend logs
Problem: High latency or slow responses
Solutions:
- Check metrics endpoint for bottlenecks
- Verify backend performance
- Increase health check timeout
- Review system resources (CPU, memory, network)
For more troubleshooting tips, see the Chinese User Manual.
- WebSocket support
- gRPC proxy support
- Rate limiting
- Circuit breaking
- Request/response transformation
- Admin web UI
Contributions are welcome! Please ensure:
- All tests pass:
cargo test - Code is formatted:
cargo fmt - No linter warnings:
cargo clippy
This project is built on Cloudflare's Pingora framework. Please review the Pingora license when using in production.
- Built with Cloudflare Pingora
- Inspired by modern proxy solutions like NGINX, Envoy, and HAProxy