diff --git a/README.md b/README.md index fce3775..c4e5639 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,375 @@ -# haproxy-mcp +# HAProxy Dynamic Load Balancer Management Suite -HAProxy Dynamic Load Balancer Management with MCP Interface +Runtime management of HAProxy without restarts. Provides an MCP (Model Context Protocol) interface for dynamic domain/server management with zero-reload operations. -## Overview +## Key Features -MCP server for managing HAProxy load balancer configurations dynamically. +- **Zero-reload domain management** using HAProxy map files and pool backends +- **Dynamic server management** with auto-persistence +- **SSL/TLS certificate management** with zero-downtime deployment via Runtime API +- **Pool sharing** for multiple domains pointing to the same backend +- **HTTP/3 (QUIC)** support alongside HTTP/1.1 and HTTP/2 +- **29 MCP tools** across domain, server, health, monitoring, config, and certificate management +- **IPv4 and IPv6** support -## Features +## Architecture -- Dynamic backend management -- SSL certificate handling -- Health checks -- Domain routing +``` +┌──────────────────────────────────────────────────┐ +│ HAProxy (host network) │ +│ TCP 80/443 (HTTP/1.1, HTTP/2) + UDP 443 (HTTP/3) │ +│ Runtime API: localhost:9999 │ +│ MCP Proxy: mcp.inouter.com → localhost:8000 │ +└──────────────┬───────────────────────────────────┘ + │ + ┌─────▼─────┐ + │ MCP │ + │ Server │ + │ :8000 │ + └───────────┘ +``` + +### Domain Routing Flow + +``` +Request → HAProxy Frontend + ↓ +domains.map lookup (Host header) + ↓ +pool_N backend (N = 1-100) + ↓ +Server (from servers.json, auto-restored on startup) +``` + +### Pool-Based Routing + +- Domains map to pools: `example.com` → `pool_N` +- 100 pools available: `pool_1` to `pool_100` +- Each pool has 10 server slots (`pool_N_1` to `pool_N_10`) + +## Prerequisites + +- **HAProxy 3.x** (running as Podman Quadlet container) +- **Python 3.11+** +- **uv** (Python package manager) +- **acme.sh** (for SSL certificate management) +- **Cloudflare** DNS (for DNS-01 ACME challenges) + +## Installation + +1. Clone the repository to `/opt/haproxy/` + +2. Install dependencies: + ```bash + cd /opt/haproxy/haproxy_mcp + uv sync + ``` + +3. Start HAProxy: + ```bash + systemctl start haproxy + ``` + +4. Start the MCP server: + ```bash + systemctl start haproxy-mcp + ``` + +## Services + +### HAProxy (Podman Quadlet) + +```bash +systemctl start haproxy # Start +systemctl stop haproxy # Stop +systemctl restart haproxy # Restart +systemctl status haproxy # Status +``` + +- Quadlet config: `/etc/containers/systemd/haproxy.container` +- Network mode: `host` + +### MCP Server (systemd) + +```bash +systemctl start haproxy-mcp # Start +systemctl stop haproxy-mcp # Stop +systemctl restart haproxy-mcp # Restart +journalctl -u haproxy-mcp -f # Logs +``` + +- Transport: `streamable-http` +- Internal: `http://localhost:8000/mcp` +- External: `https://mcp.inouter.com/mcp` (via HAProxy) + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `MCP_HOST` | `0.0.0.0` | MCP server bind host | +| `MCP_PORT` | `8000` | MCP server port | +| `HAPROXY_HOST` | `localhost` | HAProxy Runtime API host | +| `HAPROXY_PORT` | `9999` | HAProxy Runtime API port | +| `HAPROXY_STATE_FILE` | `/opt/haproxy/data/servers.state` | State file path | +| `HAPROXY_MAP_FILE` | `/opt/haproxy/conf/domains.map` | Map file path | +| `HAPROXY_SERVERS_FILE` | `/opt/haproxy/conf/servers.json` | Servers config path | +| `HAPROXY_POOL_COUNT` | `100` | Number of pool backends | +| `HAPROXY_MAX_SLOTS` | `10` | Max servers per pool | +| `HAPROXY_CONTAINER` | `haproxy` | Podman container name | +| `LOG_LEVEL` | `INFO` | Logging level (DEBUG/INFO/WARNING/ERROR) | ## Usage -Configure as MCP server in Claude Code settings. +### Zero-Reload Domain Management + +#### Adding a Domain + +```python +haproxy_add_domain("example.com", ip="10.0.0.1") +# Creates: pool_N_1 → 10.0.0.1:80 + +haproxy_add_domain("api.example.com", ip="10.0.0.1", http_port=8080) +# Creates: pool_N_1 → 10.0.0.1:8080 +``` + +This will: +1. Find an available pool (e.g., `pool_5`) +2. Add to `domains.map`: `example.com pool_5` +3. Update HAProxy runtime map via Runtime API +4. Configure server in pool (HTTP only) +5. Save to `servers.json` for persistence + +**No HAProxy reload required!** + +#### Pool Sharing + +Multiple domains can share the same pool/backend servers: + +```python +# First domain gets its own pool +haproxy_add_domain("example.com", ip="10.0.0.1") +# Creates: example.com → pool_5 + +# Additional domains share the same pool +haproxy_add_domain("www.example.com", share_with="example.com") +haproxy_add_domain("api.example.com", share_with="example.com") +# Both use: pool_5 (same backend servers) +``` + +Benefits: +- Saves pool slots (100 pools can serve unlimited domains) +- Multiple domains share the same backend servers +- Removing a shared domain keeps servers intact + +#### Bulk Server Operations + +```python +haproxy_add_servers("api.example.com", '[ + {"slot": 1, "ip": "10.0.0.1", "http_port": 80}, + {"slot": 2, "ip": "10.0.0.2", "http_port": 80}, + {"slot": 3, "ip": "2001:db8::1", "http_port": 80} +]') +``` + +### SSL/TLS Certificates + +Certificates are managed via acme.sh with Cloudflare DNS validation and deployed to HAProxy with zero downtime using the Runtime API. + +| Item | Value | +|------|-------| +| ACME Client | acme.sh (Google Trust Services CA) | +| Cert Directory | `/opt/haproxy/certs/` | +| DNS Validation | Cloudflare | + +```python +# Issue a new certificate (with wildcard) +haproxy_issue_cert("example.com", wildcard=True) + +# Renew a certificate +haproxy_renew_cert("example.com") + +# List all certificates +haproxy_list_certs() +``` + +### Health Checks + +```python +# System health +haproxy_health() +# Returns: {"status": "healthy", "components": {...}} + +# Domain-specific health +haproxy_domain_health("api.example.com") +# Returns: {"status": "healthy", "servers": [...], "healthy_count": 1} +``` + +Status values: `healthy` (all UP), `degraded` (partial UP), `down` (all DOWN), `no_servers` + +## MCP Tools (29 total) + +### Domain Management + +| Tool | Description | +|------|-------------| +| `haproxy_list_domains` | List domains (use `include_wildcards=true` for wildcards) | +| `haproxy_add_domain` | Add domain to pool (no reload), supports `share_with` for pool sharing | +| `haproxy_remove_domain` | Remove domain from pool (no reload) | + +### Server Management + +| Tool | Description | +|------|-------------| +| `haproxy_list_servers` | List servers for a domain | +| `haproxy_add_server` | Add server to slot (1-10 or 0=auto), auto-saved | +| `haproxy_add_servers` | Bulk add servers (JSON array), auto-saved | +| `haproxy_remove_server` | Remove server from slot, auto-saved | +| `haproxy_set_server_state` | Set state: ready/drain/maint | +| `haproxy_set_server_weight` | Set weight (0-256) | +| `haproxy_set_domain_state` | Set all servers of domain to ready/drain/maint | +| `haproxy_wait_drain` | Wait for connections to drain (timeout configurable) | + +### Health Check + +| Tool | Description | +|------|-------------| +| `haproxy_health` | System health (MCP, HAProxy, config files) | +| `haproxy_domain_health` | Domain-specific health with server status | +| `haproxy_get_server_health` | Get UP/DOWN/MAINT status for all servers | + +### Monitoring + +| Tool | Description | +|------|-------------| +| `haproxy_stats` | HAProxy statistics | +| `haproxy_backends` | List backends | +| `haproxy_list_frontends` | List frontends with status | +| `haproxy_get_connections` | Active connections per server | + +### Configuration + +| Tool | Description | +|------|-------------| +| `haproxy_reload` | Validate, reload config, auto-restore servers | +| `haproxy_check_config` | Validate config syntax | +| `haproxy_save_state` | Save server state to disk | +| `haproxy_restore_state` | Restore state from disk | + +### Certificate Management (Zero-Downtime) + +| Tool | Description | +|------|-------------| +| `haproxy_list_certs` | List all certificates with expiry info | +| `haproxy_cert_info` | Get detailed certificate info (expiry, issuer, SANs) | +| `haproxy_issue_cert` | Issue new certificate via acme.sh + Cloudflare DNS | +| `haproxy_renew_cert` | Renew specific certificate (force option available) | +| `haproxy_renew_all_certs` | Renew all certificates due for renewal | +| `haproxy_delete_cert` | Delete certificate from acme.sh and HAProxy | +| `haproxy_load_cert` | Load/reload certificate into HAProxy (manual trigger) | + +## MCP Connection + +**Tailscale (no auth):** +```bash +claude mcp add --transport http haproxy http://100.108.39.107:8000/mcp +``` + +**External (with auth):** +```bash +curl -X POST https://mcp.inouter.com/mcp \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":0}' +``` + +Access rules: +| Source | Token Required | +|--------|----------------| +| Tailscale (100.64.0.0/10) | No | +| External | Yes | + +Token location: `/opt/haproxy/conf/mcp-token.env` + +## Safety Features + +- **Atomic file writes**: Temp file + rename prevents corruption +- **File locking**: Prevents race conditions on concurrent operations +- **Disk-first pattern**: Config saved before HAProxy update, rollback on failure +- **Command validation**: HAProxy responses checked for errors +- **Input validation**: Domain format, IP (v4/v6), port range, slot limits +- **Bulk limits**: Max 10 servers per bulk add, 10KB JSON size limit +- **Command batching**: Multiple HAProxy commands sent in single TCP connection + +## Directory Structure + +``` +/opt/haproxy/ +├── haproxy_mcp/ # MCP server package (streamable-http) +│ ├── server.py # Main entry point +│ ├── config.py # Configuration and constants +│ ├── exceptions.py # Exception classes +│ ├── validation.py # Input validation +│ ├── haproxy_client.py # HAProxy Runtime API client +│ ├── file_ops.py # File I/O operations +│ ├── utils.py # Parsing utilities +│ └── tools/ # MCP tools (29 total) +│ ├── domains.py # Domain management (3 tools) +│ ├── servers.py # Server management (7 tools) +│ ├── health.py # Health checks (3 tools) +│ ├── monitoring.py # Monitoring (4 tools) +│ ├── configuration.py # Config management (4 tools) +│ └── certificates.py # Certificate management (7 tools) +├── conf/ +│ ├── haproxy.cfg # Main HAProxy config (100 pool backends) +│ ├── domains.map # Domain → Pool mapping +│ ├── servers.json # Server persistence (auto-managed) +│ ├── certificates.json # Certificate domain list (auto-managed) +│ └── mcp-token.env # Bearer token for MCP auth +├── certs/ # SSL/TLS certificates (HAProxy PEM format) +├── data/ # Legacy state files +├── scripts/ # Utility scripts +└── run/ # Runtime socket files +``` + +## Startup Sequence + +``` +1. systemd starts haproxy.service + ↓ +2. HAProxy loads haproxy.cfg (100 pool backends) + ↓ +3. HAProxy loads domains.map (domain → pool mapping) + ↓ +4. systemd starts haproxy-mcp.service + ↓ +5. MCP server reads servers.json & certificates.json + ↓ +6. MCP server restores servers via Runtime API + ↓ +7. MCP server loads certificates via Runtime API (zero-downtime) + ↓ +8. Ready to serve traffic +``` + +## HAProxy Runtime API + +```bash +# Show domain mappings +echo "show map /usr/local/etc/haproxy/domains.map" | nc localhost 9999 + +# Add domain mapping +echo "add map /usr/local/etc/haproxy/domains.map example.com pool_5" | nc localhost 9999 + +# Show server state +echo "show servers state" | nc localhost 9999 + +# Set server address +echo "set server pool_1/pool_1_1 addr 10.0.0.1 port 80" | nc localhost 9999 + +# Enable server +echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999 +``` ## License