secubox-openwrt/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl
CyberMind-FR fed7bd43c1 fix(haproxy): Combine fullchain + key for HAProxy certificates
HAProxy requires certificate files to contain both the fullchain
(cert + intermediate CA) and the private key concatenated together.

Changes:
- haproxyctl: Fix cert_add to create combined .pem files
- haproxy-sync-certs: New script to sync ACME certs to HAProxy format
- haproxy.sh: ACME deploy hook for HAProxy
- init.d: Sync certs before starting HAProxy
- Makefile: Install new scripts, add cron job for cert sync

This fixes the "No Private Key found" error when HAProxy tries to
load certificates that only contain the fullchain without the key.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:29 +01:00

1040 lines
26 KiB
Bash

#!/bin/sh
# SecuBox HAProxy Controller
# Copyright (C) 2025 CyberMind.fr
# Source OpenWrt functions for UCI iteration
. /lib/functions.sh
CONFIG="haproxy"
LXC_NAME="haproxy"
# Paths
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
DATA_PATH="/srv/haproxy"
SHARE_PATH="/usr/share/haproxy"
CERTS_PATH="$DATA_PATH/certs"
CONFIG_PATH="$DATA_PATH/config"
# Logging
log_info() { echo "[INFO] $*"; logger -t haproxy "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t haproxy -p err "$*"; }
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }
# Helpers
require_root() {
[ "$(id -u)" -eq 0 ] || { log_error "Root required"; exit 1; }
}
has_lxc() { command -v lxc-start >/dev/null 2>&1; }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load configuration
load_config() {
http_port="$(uci_get main.http_port)" || http_port="80"
https_port="$(uci_get main.https_port)" || https_port="443"
stats_port="$(uci_get main.stats_port)" || stats_port="8404"
stats_enabled="$(uci_get main.stats_enabled)" || stats_enabled="1"
stats_user="$(uci_get main.stats_user)" || stats_user="admin"
stats_password="$(uci_get main.stats_password)" || stats_password="secubox"
data_path="$(uci_get main.data_path)" || data_path="$DATA_PATH"
memory_limit="$(uci_get main.memory_limit)" || memory_limit="256M"
maxconn="$(uci_get main.maxconn)" || maxconn="4096"
log_level="$(uci_get main.log_level)" || log_level="warning"
CERTS_PATH="$data_path/certs"
CONFIG_PATH="$data_path/config"
ensure_dir "$data_path"
ensure_dir "$CERTS_PATH"
ensure_dir "$CONFIG_PATH"
}
# Usage
usage() {
cat <<EOF
SecuBox HAProxy Controller
Usage: $(basename $0) <command> [options]
Container Commands:
install Setup HAProxy LXC container
uninstall Remove container (keeps config)
update Update HAProxy in container
status Show service status
Configuration:
generate Generate haproxy.cfg from UCI
validate Validate configuration
reload Reload HAProxy config (no downtime)
Virtual Hosts:
vhost list List all virtual hosts
vhost add <domain> Add virtual host
vhost remove <domain> Remove virtual host
vhost sync Sync vhosts to config
Backends:
backend list List all backends
backend add <name> Add backend
backend remove <name> Remove backend
Servers:
server list <backend> List servers in backend
server add <backend> <addr:port> Add server to backend
server remove <backend> <name> Remove server
Certificates:
cert list List certificates
cert add <domain> Request ACME certificate
cert import <domain> <cert> <key> Import certificate
cert renew [domain] Renew certificate(s)
cert remove <domain> Remove certificate
Service Commands:
service-run Run in foreground (for init)
service-stop Stop service
Stats:
stats Show HAProxy stats
connections Show active connections
EOF
}
# ===========================================
# LXC Container Management
# ===========================================
lxc_running() {
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}
lxc_exists() {
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
}
lxc_stop() {
if lxc_running; then
log_info "Stopping HAProxy container..."
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
sleep 2
fi
}
lxc_create_rootfs() {
log_info "Creating Alpine rootfs for HAProxy..."
ensure_dir "$LXC_PATH/$LXC_NAME"
local arch="x86_64"
case "$(uname -m)" in
aarch64) arch="aarch64" ;;
armv7l) arch="armv7" ;;
esac
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/$arch/alpine-minirootfs-3.21.2-$arch.tar.gz"
local rootfs_tar="/tmp/alpine-haproxy.tar.gz"
log_info "Downloading Alpine rootfs..."
wget -q -O "$rootfs_tar" "$alpine_url" || {
log_error "Failed to download Alpine rootfs"
return 1
}
log_info "Extracting rootfs..."
ensure_dir "$LXC_ROOTFS"
tar -xzf "$rootfs_tar" -C "$LXC_ROOTFS" || {
log_error "Failed to extract rootfs"
return 1
}
rm -f "$rootfs_tar"
# Configure Alpine
cat > "$LXC_ROOTFS/etc/resolv.conf" << 'EOF'
nameserver 1.1.1.1
nameserver 8.8.8.8
EOF
cat > "$LXC_ROOTFS/etc/apk/repositories" << 'EOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
EOF
# Install HAProxy
log_info "Installing HAProxy..."
chroot "$LXC_ROOTFS" /bin/sh -c "
apk update
apk add --no-cache haproxy openssl curl socat lua5.4 lua5.4-socket
" || {
log_error "Failed to install HAProxy"
return 1
}
log_info "Rootfs created successfully"
}
lxc_create_config() {
load_config
local arch="x86_64"
case "$(uname -m)" in
aarch64) arch="aarch64" ;;
armv7l) arch="armhf" ;;
esac
local mem_bytes=$(echo "$memory_limit" | sed 's/M/000000/;s/G/000000000/')
cat > "$LXC_CONFIG" << EOF
# HAProxy LXC Configuration
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_ROOTFS
lxc.arch = $arch
# Network: use host network for binding ports
lxc.net.0.type = none
# Mount points
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.mount.entry = $data_path opt/haproxy none bind,create=dir 0 0
# Environment
lxc.environment = HTTP_PORT=$http_port
lxc.environment = HTTPS_PORT=$https_port
lxc.environment = STATS_PORT=$stats_port
# Security
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time
# Resource limits (cgroup2)
lxc.cgroup2.memory.max = $mem_bytes
# Init command
lxc.init.cmd = /opt/start-haproxy.sh
EOF
log_info "LXC config created"
}
lxc_run() {
load_config
lxc_stop
if ! lxc_exists; then
log_error "Container not installed. Run: haproxyctl install"
return 1
fi
lxc_create_config
# Ensure start script exists
local start_script="$LXC_ROOTFS/opt/start-haproxy.sh"
cat > "$start_script" << 'STARTEOF'
#!/bin/sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
CONFIG_FILE="/opt/haproxy/config/haproxy.cfg"
LOG_FILE="/opt/haproxy/startup.log"
# Log all output
exec >>"$LOG_FILE" 2>&1
echo "=== HAProxy startup: $(date) ==="
echo "Config: $CONFIG_FILE"
ls -la /opt/haproxy/
ls -la /opt/haproxy/certs/ 2>/dev/null || echo "No certs dir"
# Wait for config
if [ ! -f "$CONFIG_FILE" ]; then
echo "[haproxy] Config not found, generating default..."
mkdir -p /opt/haproxy/config
cat > "$CONFIG_FILE" << 'CFGEOF'
global
log stdout format raw local0
maxconn 4096
stats socket /var/run/haproxy.sock mode 660 level admin expose-fd listeners
stats timeout 30s
defaults
mode http
log global
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
frontend stats
bind *:8404
mode http
stats enable
stats uri /stats
stats refresh 10s
stats admin if TRUE
frontend http-in
bind *:80
mode http
default_backend fallback
backend fallback
mode http
server local 127.0.0.1:8080 check
CFGEOF
fi
# Validate config first
echo "[haproxy] Validating config..."
haproxy -c -f "$CONFIG_FILE"
RC=$?
echo "[haproxy] Validation exit code: $RC"
if [ $RC -ne 0 ]; then
echo "[haproxy] Config validation failed!"
exit 1
fi
echo "[haproxy] Starting HAProxy..."
exec haproxy -f "$CONFIG_FILE" -W -db
STARTEOF
chmod +x "$start_script"
# Generate config before starting
generate_config
log_info "Starting HAProxy container..."
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}
lxc_exec() {
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc-attach -n "$LXC_NAME" -- "$@"
}
# ===========================================
# Configuration Generation
# ===========================================
generate_config() {
load_config
local cfg_file="$CONFIG_PATH/haproxy.cfg"
log_info "Generating HAProxy configuration..."
# Global section
cat > "$cfg_file" << EOF
# HAProxy Configuration - Generated by SecuBox
# DO NOT EDIT - Use UCI configuration
global
log stdout format raw local0 $log_level
maxconn $maxconn
stats socket /var/run/haproxy.sock mode 660 level admin expose-fd listeners
stats timeout 30s
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
tune.ssl.default-dh-param 2048
EOF
# Defaults section
local mode=$(uci_get defaults.mode) || mode="http"
local timeout_connect=$(uci_get defaults.timeout_connect) || timeout_connect="5s"
local timeout_client=$(uci_get defaults.timeout_client) || timeout_client="30s"
local timeout_server=$(uci_get defaults.timeout_server) || timeout_server="30s"
cat >> "$cfg_file" << EOF
defaults
mode $mode
log global
option httplog
option dontlognull
option forwardfor
timeout connect $timeout_connect
timeout client $timeout_client
timeout server $timeout_server
timeout http-request 10s
timeout http-keep-alive 10s
retries 3
EOF
# Stats frontend
if [ "$stats_enabled" = "1" ]; then
cat >> "$cfg_file" << EOF
frontend stats
bind *:$stats_port
mode http
stats enable
stats uri /stats
stats refresh 10s
stats auth $stats_user:$stats_password
stats admin if TRUE
EOF
fi
# Generate frontends from UCI
_generate_frontends >> "$cfg_file"
# Generate backends from UCI
_generate_backends >> "$cfg_file"
log_info "Configuration generated: $cfg_file"
}
_generate_frontends() {
# HTTP Frontend
cat << EOF
frontend http-in
bind *:$http_port
mode http
EOF
# Add HTTPS redirect rules for vhosts with ssl_redirect
config_load haproxy
config_foreach _add_ssl_redirect vhost
# Add vhost ACLs for HTTP
config_foreach _add_vhost_acl vhost "http"
echo " default_backend fallback"
echo ""
# HTTPS Frontend (if certificates exist)
# Use container path /opt/haproxy/certs/ (not host path)
local CONTAINER_CERTS_PATH="/opt/haproxy/certs"
if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
cat << EOF
frontend https-in
bind *:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
mode http
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
EOF
# Add vhost ACLs for HTTPS
config_foreach _add_vhost_acl vhost "https"
echo " default_backend fallback"
echo ""
fi
}
_add_ssl_redirect() {
local section="$1"
local enabled domain ssl_redirect
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get domain "$section" domain
config_get ssl_redirect "$section" ssl_redirect "0"
[ -n "$domain" ] || return
[ "$ssl_redirect" = "1" ] || return
local acl_name=$(echo "$domain" | tr '.' '_' | tr '-' '_')
echo " acl host_${acl_name} hdr(host) -i $domain"
echo " http-request redirect scheme https code 301 if host_${acl_name} !{ ssl_fc }"
}
_add_vhost_acl() {
local section="$1"
local proto="$2"
local enabled domain backend ssl
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get domain "$section" domain
config_get backend "$section" backend
config_get ssl "$section" ssl "0"
[ -n "$domain" ] || return
[ -n "$backend" ] || return
# For HTTP frontend, skip SSL-only vhosts
[ "$proto" = "http" ] && [ "$ssl" = "1" ] && return
local acl_name=$(echo "$domain" | tr '.' '_' | tr '-' '_')
echo " acl host_${acl_name} hdr(host) -i $domain"
echo " use_backend $backend if host_${acl_name}"
}
_generate_backends() {
config_load haproxy
# Generate each backend from UCI
config_foreach _generate_backend backend
# Only add default fallback if no "fallback" backend exists in UCI
if ! uci -q get haproxy.fallback >/dev/null 2>&1; then
cat << EOF
backend fallback
mode http
http-request deny deny_status 503
EOF
fi
}
_generate_backend() {
local section="$1"
local enabled name mode balance health_check
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get name "$section" name "$section"
config_get mode "$section" mode "http"
config_get balance "$section" balance "roundrobin"
config_get health_check "$section" health_check ""
echo ""
echo "backend $name"
echo " mode $mode"
echo " balance $balance"
[ -n "$health_check" ] && echo " option $health_check"
# Add servers defined in backend section (handles both single and list)
local server_line
config_get server_line "$section" server ""
[ -n "$server_line" ] && echo " server $server_line"
# Add servers from separate server UCI sections
config_foreach _add_server_to_backend server "$name"
}
_add_server_to_backend() {
local section="$1"
local target_backend="$2"
local backend server_name address port weight check enabled
config_get backend "$section" backend
[ "$backend" = "$target_backend" ] || return
config_get enabled "$section" enabled "0"
[ "$enabled" = "1" ] || return
config_get server_name "$section" name "$section"
config_get address "$section" address
config_get port "$section" port "80"
config_get weight "$section" weight "100"
config_get check "$section" check "1"
[ -n "$address" ] || return
local check_opt=""
[ "$check" = "1" ] && check_opt="check"
echo " server $server_name $address:$port weight $weight $check_opt"
}
# ===========================================
# Certificate Management
# ===========================================
cmd_cert_list() {
load_config
echo "Certificates in $CERTS_PATH:"
echo "----------------------------"
if [ -d "$CERTS_PATH" ]; then
for cert in "$CERTS_PATH"/*.pem; do
[ -f "$cert" ] || continue
local name=$(basename "$cert" .pem)
local expiry=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2)
echo " $name - Expires: ${expiry:-Unknown}"
done
else
echo " No certificates found"
fi
}
cmd_cert_add() {
require_root
load_config
local domain="$1"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
local email=$(uci_get acme.email)
local staging=$(uci_get acme.staging)
local key_type_raw=$(uci_get acme.key_type) || key_type_raw="ec-256"
# Convert key type for acme.sh (rsa-4096 → 4096, ec-256 stays ec-256)
local key_type="$key_type_raw"
case "$key_type_raw" in
rsa-*) key_type="${key_type_raw#rsa-}" ;; # rsa-4096 → 4096
RSA-*) key_type="${key_type_raw#RSA-}" ;;
esac
[ -z "$email" ] && { log_error "ACME email not configured. Set in LuCI > Services > HAProxy > Settings"; return 1; }
log_info "Requesting certificate for $domain..."
local staging_flag=""
[ "$staging" = "1" ] && staging_flag="--staging"
# Find acme.sh - check OpenWrt location first, then PATH
local ACME_SH=""
if [ -x "/usr/lib/acme/client/acme.sh" ]; then
ACME_SH="/usr/lib/acme/client/acme.sh"
elif command -v acme.sh >/dev/null 2>&1; then
ACME_SH="acme.sh"
fi
if [ -n "$ACME_SH" ]; then
# Set acme.sh home directory
export LE_WORKING_DIR="/etc/acme"
export LE_CONFIG_HOME="/etc/acme"
ensure_dir "$LE_WORKING_DIR"
# Register account if needed
if [ ! -f "$LE_WORKING_DIR/account.conf" ]; then
log_info "Registering ACME account..."
"$ACME_SH" --register-account -m "$email" $staging_flag --home "$LE_WORKING_DIR" || true
fi
# Check if HAProxy is using the port
local haproxy_was_running=0
if lxc_running; then
log_info "Temporarily stopping HAProxy for certificate issuance..."
haproxy_was_running=1
/etc/init.d/haproxy stop 2>/dev/null || true
sleep 2
fi
# Issue certificate using standalone mode
log_info "Issuing certificate (standalone mode on port $http_port)..."
local acme_result=0
"$ACME_SH" --issue -d "$domain" \
--standalone --httpport "$http_port" \
--keylength "$key_type" \
$staging_flag \
--home "$LE_WORKING_DIR" || acme_result=$?
# acme.sh returns 0 on success, 2 on "skip/already valid" - both are OK
# Install the certificate to our certs path
if [ "$acme_result" -eq 0 ] || [ "$acme_result" -eq 2 ]; then
log_info "Installing certificate..."
"$ACME_SH" --install-cert -d "$domain" \
--home "$LE_WORKING_DIR" \
--cert-file "$CERTS_PATH/$domain.crt" \
--key-file "$CERTS_PATH/$domain.key" \
--fullchain-file "$CERTS_PATH/$domain.fullchain.pem" \
--reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true
# HAProxy needs combined file: fullchain + private key
log_info "Creating combined PEM for HAProxy..."
cat "$CERTS_PATH/$domain.fullchain.pem" "$CERTS_PATH/$domain.key" > "$CERTS_PATH/$domain.pem"
chmod 600 "$CERTS_PATH/$domain.pem"
fi
# Restart HAProxy if it was running
if [ "$haproxy_was_running" = "1" ]; then
log_info "Restarting HAProxy..."
/etc/init.d/haproxy start 2>/dev/null || true
fi
# Check if certificate was created
if [ ! -f "$CERTS_PATH/$domain.pem" ]; then
log_error "Certificate issuance failed. Ensure port $http_port is accessible from internet and domain points to this IP."
return 1
fi
log_info "Certificate ready: $CERTS_PATH/$domain.pem"
elif command -v certbot >/dev/null 2>&1; then
certbot certonly --standalone -d "$domain" \
--email "$email" --agree-tos -n \
--http-01-port "$http_port" $staging_flag || {
log_error "Certbot failed"
return 1
}
# Copy to HAProxy certs dir
local le_path="/etc/letsencrypt/live/$domain"
cat "$le_path/fullchain.pem" "$le_path/privkey.pem" > "$CERTS_PATH/$domain.pem"
else
log_error "No ACME client found. Install: opkg install acme acme-acmesh"
return 1
fi
chmod 600 "$CERTS_PATH/$domain.pem"
# Add to UCI
local section="cert_$(echo "$domain" | tr '.-' '__')"
uci set haproxy.$section=certificate
uci set haproxy.$section.domain="$domain"
uci set haproxy.$section.type="acme"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Certificate installed for $domain"
}
cmd_cert_import() {
require_root
load_config
local domain="$1"
local cert_file="$2"
local key_file="$3"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
[ -z "$cert_file" ] && { log_error "Certificate file required"; return 1; }
[ -z "$key_file" ] && { log_error "Key file required"; return 1; }
[ -f "$cert_file" ] || { log_error "Certificate file not found"; return 1; }
[ -f "$key_file" ] || { log_error "Key file not found"; return 1; }
# Combine cert and key for HAProxy
cat "$cert_file" "$key_file" > "$CERTS_PATH/$domain.pem"
chmod 600 "$CERTS_PATH/$domain.pem"
# Add to UCI
uci set haproxy.cert_${domain//[.-]/_}=certificate
uci set haproxy.cert_${domain//[.-]/_}.domain="$domain"
uci set haproxy.cert_${domain//[.-]/_}.type="manual"
uci set haproxy.cert_${domain//[.-]/_}.enabled="1"
uci commit haproxy
log_info "Certificate imported for $domain"
}
# ===========================================
# Virtual Host Management
# ===========================================
cmd_vhost_list() {
load_config
echo "Virtual Hosts:"
echo "--------------"
config_load haproxy
config_foreach _print_vhost vhost
}
_print_vhost() {
local section="$1"
local enabled domain backend ssl ssl_redirect acme
config_get domain "$section" domain
config_get backend "$section" backend
config_get enabled "$section" enabled "0"
config_get ssl "$section" ssl "0"
config_get ssl_redirect "$section" ssl_redirect "0"
config_get acme "$section" acme "0"
local status="disabled"
[ "$enabled" = "1" ] && status="enabled"
local flags=""
[ "$ssl" = "1" ] && flags="${flags}SSL "
[ "$ssl_redirect" = "1" ] && flags="${flags}REDIRECT "
[ "$acme" = "1" ] && flags="${flags}ACME "
printf " %-30s -> %-20s [%s] %s\n" "$domain" "$backend" "$status" "$flags"
}
cmd_vhost_add() {
require_root
load_config
local domain="$1"
local backend="$2"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
[ -z "$backend" ] && backend="fallback"
local section="vhost_${domain//[.-]/_}"
uci set haproxy.$section=vhost
uci set haproxy.$section.domain="$domain"
uci set haproxy.$section.backend="$backend"
uci set haproxy.$section.ssl="1"
uci set haproxy.$section.ssl_redirect="1"
uci set haproxy.$section.acme="1"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Virtual host added: $domain -> $backend"
}
cmd_vhost_remove() {
require_root
local domain="$1"
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
local section="vhost_${domain//[.-]/_}"
uci delete haproxy.$section 2>/dev/null
uci commit haproxy
log_info "Virtual host removed: $domain"
}
# ===========================================
# Backend Management
# ===========================================
cmd_backend_list() {
load_config
echo "Backends:"
echo "---------"
config_load haproxy
config_foreach _print_backend backend
}
_print_backend() {
local section="$1"
local enabled name mode balance
config_get name "$section" name "$section"
config_get enabled "$section" enabled "0"
config_get mode "$section" mode "http"
config_get balance "$section" balance "roundrobin"
local status="disabled"
[ "$enabled" = "1" ] && status="enabled"
printf " %-20s mode=%-6s balance=%-12s [%s]\n" "$name" "$mode" "$balance" "$status"
}
cmd_backend_add() {
require_root
local name="$1"
[ -z "$name" ] && { log_error "Backend name required"; return 1; }
local section="backend_${name//[.-]/_}"
uci set haproxy.$section=backend
uci set haproxy.$section.name="$name"
uci set haproxy.$section.mode="http"
uci set haproxy.$section.balance="roundrobin"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Backend added: $name"
}
cmd_server_add() {
require_root
local backend="$1"
local addr_port="$2"
local server_name="$3"
[ -z "$backend" ] && { log_error "Backend name required"; return 1; }
[ -z "$addr_port" ] && { log_error "Address:port required"; return 1; }
local address=$(echo "$addr_port" | cut -d: -f1)
local port=$(echo "$addr_port" | cut -d: -f2)
[ -z "$port" ] && port="80"
[ -z "$server_name" ] && server_name="srv_$(echo $address | tr '.' '_')_$port"
local section="server_${server_name//[.-]/_}"
uci set haproxy.$section=server
uci set haproxy.$section.backend="$backend"
uci set haproxy.$section.name="$server_name"
uci set haproxy.$section.address="$address"
uci set haproxy.$section.port="$port"
uci set haproxy.$section.weight="100"
uci set haproxy.$section.check="1"
uci set haproxy.$section.enabled="1"
uci commit haproxy
log_info "Server added: $server_name ($address:$port) to backend $backend"
}
# ===========================================
# Commands
# ===========================================
cmd_install() {
require_root
load_config
log_info "Installing HAProxy..."
has_lxc || { log_error "LXC not installed"; exit 1; }
if ! lxc_exists; then
lxc_create_rootfs || exit 1
fi
lxc_create_config || exit 1
log_info "Installation complete!"
log_info ""
log_info "Next steps:"
log_info " 1. Enable: uci set haproxy.main.enabled=1 && uci commit haproxy"
log_info " 2. Add vhost: haproxyctl vhost add example.com backend_name"
log_info " 3. Start: /etc/init.d/haproxy start"
}
cmd_status() {
load_config
local enabled=$(uci_get main.enabled)
local running="no"
lxc_running && running="yes"
cat << EOF
HAProxy Status
==============
Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")
Running: $running
HTTP Port: $http_port
HTTPS Port: $https_port
Stats Port: $stats_port
Stats URL: http://localhost:$stats_port/stats
Container: $LXC_NAME
Rootfs: $LXC_ROOTFS
Config: $CONFIG_PATH/haproxy.cfg
Certs: $CERTS_PATH
EOF
}
cmd_reload() {
require_root
if ! lxc_running; then
log_error "Container not running"
return 1
fi
generate_config
log_info "Reloading HAProxy configuration..."
# HAProxy in master-worker mode (-W) reloads gracefully on SIGUSR2
# Fallback to SIGHUP if USR2 fails
lxc_exec killall -USR2 haproxy 2>/dev/null || \
lxc_exec killall -HUP haproxy 2>/dev/null || \
log_error "Could not signal HAProxy for reload"
log_info "Reload complete"
}
cmd_validate() {
load_config
generate_config
log_info "Validating configuration..."
if lxc_running; then
lxc_exec haproxy -c -f /opt/haproxy/config/haproxy.cfg
else
# Validate locally if possible
if [ -f "$CONFIG_PATH/haproxy.cfg" ]; then
log_info "Config file: $CONFIG_PATH/haproxy.cfg"
head -50 "$CONFIG_PATH/haproxy.cfg"
fi
fi
}
cmd_stats() {
if ! lxc_running; then
log_error "Container not running"
return 1
fi
lxc_exec sh -c "echo 'show stat' | socat stdio /var/run/haproxy.sock" 2>/dev/null || \
curl -s "http://localhost:$stats_port/stats;csv"
}
cmd_service_run() {
require_root
load_config
has_lxc || { log_error "LXC not installed"; exit 1; }
lxc_run
}
cmd_service_stop() {
require_root
lxc_stop
}
# ===========================================
# Main
# ===========================================
case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; lxc_stop; log_info "Uninstall: rm -rf $LXC_PATH/$LXC_NAME" ;;
update) shift; lxc_exec apk update && lxc_exec apk upgrade haproxy ;;
status) shift; cmd_status "$@" ;;
generate) shift; generate_config "$@" ;;
validate) shift; cmd_validate "$@" ;;
reload) shift; cmd_reload "$@" ;;
vhost)
shift
case "${1:-}" in
list) shift; cmd_vhost_list "$@" ;;
add) shift; cmd_vhost_add "$@" ;;
remove) shift; cmd_vhost_remove "$@" ;;
sync) shift; generate_config && cmd_reload ;;
*) echo "Usage: haproxyctl vhost {list|add|remove|sync}" ;;
esac
;;
backend)
shift
case "${1:-}" in
list) shift; cmd_backend_list "$@" ;;
add) shift; cmd_backend_add "$@" ;;
remove) shift; uci delete haproxy.backend_${2//[.-]/_} 2>/dev/null; uci commit haproxy ;;
*) echo "Usage: haproxyctl backend {list|add|remove}" ;;
esac
;;
server)
shift
case "${1:-}" in
list) shift; config_load haproxy; config_foreach _print_server server "$1" ;;
add) shift; cmd_server_add "$@" ;;
remove) shift; uci delete haproxy.server_${3//[.-]/_} 2>/dev/null; uci commit haproxy ;;
*) echo "Usage: haproxyctl server {list|add|remove} <backend> [addr:port]" ;;
esac
;;
cert)
shift
case "${1:-}" in
list) shift; cmd_cert_list "$@" ;;
add) shift; cmd_cert_add "$@" ;;
import) shift; cmd_cert_import "$@" ;;
renew) shift; cmd_cert_add "$@" ;;
remove) shift; rm -f "$CERTS_PATH/$1.pem"; uci delete haproxy.cert_${1//[.-]/_} 2>/dev/null ;;
*) echo "Usage: haproxyctl cert {list|add|import|renew|remove}" ;;
esac
;;
stats) shift; cmd_stats "$@" ;;
connections) shift; lxc_exec sh -c "echo 'show sess' | socat stdio /var/run/haproxy.sock" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
shell) shift; lxc_exec /bin/sh ;;
exec) shift; lxc_exec "$@" ;;
*) usage ;;
esac