- Add showEditVhostModal() for editing virtual host properties - Add showEditBackendModal() for editing backend configuration - Add showEditServerModal() for editing server properties - Modern card-based UI with inline edit/delete actions - Toggle enable/disable for backends - Fix haproxyctl to read server option from backend UCI sections - Add debug logging to container startup script Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1035 lines
26 KiB
Bash
1035 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.pem" \
|
|
--reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true
|
|
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
|