- Add automatic mitmproxy route sync after vhost operations - Route through WAF by default: sets original_backend for route resolution - Add --nowaf option to bypass WAF routing if needed - Prevents missing routes when creating new vhosts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2470 lines
70 KiB
Bash
2470 lines
70 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_warn() { echo "[WARN] $*" >&2; logger -t haproxy -p warning "$*"; }
|
|
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}; }
|
|
|
|
# Firewall management - auto-open ports when publishing
|
|
firewall_ensure_haproxy_rules() {
|
|
local http_port="${1:-80}"
|
|
local https_port="${2:-443}"
|
|
local changed=0
|
|
|
|
# Check if HTTP rule exists
|
|
local http_rule_exists=0
|
|
local i=0
|
|
while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
|
|
local name=$(uci -q get firewall.@rule[$i].name)
|
|
if [ "$name" = "HAProxy-HTTP" ]; then
|
|
http_rule_exists=1
|
|
break
|
|
fi
|
|
i=$((i + 1))
|
|
done
|
|
|
|
# Create HTTP rule if missing
|
|
if [ "$http_rule_exists" = "0" ]; then
|
|
log_info "Creating firewall rule for HTTP (port $http_port)..."
|
|
uci add firewall rule
|
|
uci set firewall.@rule[-1].name='HAProxy-HTTP'
|
|
uci set firewall.@rule[-1].src='wan'
|
|
uci set firewall.@rule[-1].dest_port="$http_port"
|
|
uci set firewall.@rule[-1].proto='tcp'
|
|
uci set firewall.@rule[-1].target='ACCEPT'
|
|
uci set firewall.@rule[-1].enabled='1'
|
|
changed=1
|
|
fi
|
|
|
|
# Check if HTTPS rule exists
|
|
local https_rule_exists=0
|
|
i=0
|
|
while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
|
|
local name=$(uci -q get firewall.@rule[$i].name)
|
|
if [ "$name" = "HAProxy-HTTPS" ]; then
|
|
https_rule_exists=1
|
|
break
|
|
fi
|
|
i=$((i + 1))
|
|
done
|
|
|
|
# Create HTTPS rule if missing
|
|
if [ "$https_rule_exists" = "0" ]; then
|
|
log_info "Creating firewall rule for HTTPS (port $https_port)..."
|
|
uci add firewall rule
|
|
uci set firewall.@rule[-1].name='HAProxy-HTTPS'
|
|
uci set firewall.@rule[-1].src='wan'
|
|
uci set firewall.@rule[-1].dest_port="$https_port"
|
|
uci set firewall.@rule[-1].proto='tcp'
|
|
uci set firewall.@rule[-1].target='ACCEPT'
|
|
uci set firewall.@rule[-1].enabled='1'
|
|
changed=1
|
|
fi
|
|
|
|
# Apply changes
|
|
if [ "$changed" = "1" ]; then
|
|
uci commit firewall
|
|
/etc/init.d/firewall reload 2>/dev/null || true
|
|
log_info "Firewall rules updated - ports $http_port and $https_port open on WAN"
|
|
fi
|
|
}
|
|
|
|
firewall_check_haproxy_rules() {
|
|
local http_ok=0 https_ok=0
|
|
local i=0
|
|
while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
|
|
local name=$(uci -q get firewall.@rule[$i].name)
|
|
local enabled=$(uci -q get firewall.@rule[$i].enabled)
|
|
[ "$name" = "HAProxy-HTTP" ] && [ "$enabled" != "0" ] && http_ok=1
|
|
[ "$name" = "HAProxy-HTTPS" ] && [ "$enabled" != "0" ] && https_ok=1
|
|
i=$((i + 1))
|
|
done
|
|
[ "$http_ok" = "1" ] && [ "$https_ok" = "1" ]
|
|
}
|
|
|
|
# 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"
|
|
default_backend="$(uci_get main.default_backend)" || default_backend="default_luci"
|
|
waf_enabled="$(uci_get main.waf_enabled)" || waf_enabled="0"
|
|
waf_backend="$(uci_get main.waf_backend)" || waf_backend="mitmproxy_inspector"
|
|
|
|
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
|
|
|
|
Path ACLs:
|
|
path list List all path ACLs
|
|
path sync <prefix> <host> Auto-generate path ACLs from backends
|
|
(e.g., path sync /gk2 secubox.in)
|
|
path add <pattern> <backend> <host> Add path ACL
|
|
path remove <name> Remove path ACL
|
|
path clear <prefix> Remove all path ACLs with prefix
|
|
|
|
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
|
|
}
|
|
|
|
# Start the container in background (daemon mode)
|
|
lxc_start_bg() {
|
|
if lxc_running; then
|
|
return 0
|
|
fi
|
|
if ! lxc_exists; then
|
|
log_error "Container not installed. Run 'haproxyctl install' first."
|
|
return 1
|
|
fi
|
|
log_info "Starting HAProxy container..."
|
|
generate_config
|
|
lxc-start -n "$LXC_NAME" -d
|
|
sleep 2
|
|
if lxc_running; then
|
|
log_info "Container started"
|
|
return 0
|
|
else
|
|
log_error "Failed to start container"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Reload HAProxy config inside the container (quick reload without restart)
|
|
lxc_reload() {
|
|
if ! lxc_running; then
|
|
log_warn "Container not running, starting it..."
|
|
lxc_start_bg || return 1
|
|
fi
|
|
generate_config
|
|
lxc_exec cp /opt/haproxy/config/haproxy.cfg /etc/haproxy/haproxy.cfg 2>/dev/null || true
|
|
lxc_exec killall -USR2 haproxy 2>/dev/null || \
|
|
lxc_exec killall -HUP haproxy 2>/dev/null || true
|
|
}
|
|
|
|
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 - avoid cgroup:mixed which causes failures on some systems
|
|
lxc.mount.entry = $data_path opt/haproxy none bind,create=dir 0 0
|
|
|
|
# Disable seccomp for compatibility
|
|
lxc.seccomp.profile =
|
|
|
|
# TTY/PTY settings
|
|
lxc.tty.max = 0
|
|
lxc.pty.max = 256
|
|
|
|
# Auto device nodes
|
|
lxc.autodev = 1
|
|
|
|
# Environment
|
|
lxc.environment = HTTP_PORT=$http_port
|
|
lxc.environment = HTTPS_PORT=$https_port
|
|
lxc.environment = STATS_PORT=$stats_port
|
|
|
|
# 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"
|
|
|
|
# Clean up legacy certificate files - only .pem files should exist
|
|
# HAProxy loads all files from certs directory, and extra files cause errors
|
|
if [ -d "/opt/haproxy/certs" ]; then
|
|
for pem in /opt/haproxy/certs/*.pem; do
|
|
[ -f "$pem" ] || continue
|
|
base="${pem%.pem}"
|
|
# Remove any associated .crt, .key, .fullchain.pem, .crt.key files
|
|
rm -f "${base}.crt" "${base}.key" "${base}.crt.key" "${base}.fullchain.pem" 2>/dev/null
|
|
done
|
|
fi
|
|
|
|
# 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 default_luci
|
|
|
|
backend default_luci
|
|
mode http
|
|
balance roundrobin
|
|
server luci 192.168.255.1:8081 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" -- "$@"
|
|
}
|
|
|
|
# ===========================================
|
|
# Certificate List Generation (for multi-cert SNI)
|
|
# ===========================================
|
|
|
|
# Generate crt-list file for multi-certificate SNI support
|
|
# This is more reliable than directory mode for multi-domain setups
|
|
generate_certs_list() {
|
|
local certs_list="$CERTS_PATH/certs.list"
|
|
local container_certs_path="/opt/haproxy/certs"
|
|
|
|
log_info "Generating certificate list file..."
|
|
|
|
# Start fresh
|
|
> "$certs_list"
|
|
|
|
# Process each .pem file
|
|
for cert_file in "$CERTS_PATH"/*.pem; do
|
|
[ -f "$cert_file" ] || continue
|
|
|
|
local filename=$(basename "$cert_file")
|
|
local container_cert_path="$container_certs_path/$filename"
|
|
|
|
# Extract domain names from certificate (CN and SANs)
|
|
local domains=$(openssl x509 -in "$cert_file" -noout -text 2>/dev/null | \
|
|
grep -A1 "Subject Alternative Name" | \
|
|
tail -n1 | \
|
|
sed 's/DNS://g' | \
|
|
tr ',' '\n' | \
|
|
tr -d ' ')
|
|
|
|
# If no SANs, try to get CN
|
|
if [ -z "$domains" ]; then
|
|
domains=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | \
|
|
sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')
|
|
fi
|
|
|
|
if [ -n "$domains" ]; then
|
|
# For each domain, add an entry with SNI filter
|
|
# Format: /path/to/cert.pem [sni_filter]
|
|
echo "$domains" | while read -r domain; do
|
|
[ -n "$domain" ] || continue
|
|
# Handle wildcard certificates - the SNI filter should match the pattern
|
|
echo "$container_cert_path $domain" >> "$certs_list"
|
|
done
|
|
else
|
|
# Fallback: add cert without SNI filter (will be default)
|
|
log_warn "No domain found in certificate: $filename"
|
|
echo "$container_cert_path" >> "$certs_list"
|
|
fi
|
|
done
|
|
|
|
# Deduplicate entries (same cert may have multiple SANs)
|
|
if [ -f "$certs_list" ]; then
|
|
sort -u "$certs_list" > "$certs_list.tmp"
|
|
mv "$certs_list.tmp" "$certs_list"
|
|
|
|
local count=$(wc -l < "$certs_list")
|
|
log_info "Generated certs.list with $count entries"
|
|
else
|
|
log_warn "No certificates found in $CERTS_PATH"
|
|
fi
|
|
}
|
|
|
|
# ===========================================
|
|
# Configuration Generation
|
|
# ===========================================
|
|
|
|
# Helper to print a single userlist from UCI
|
|
_print_uci_userlist() {
|
|
local section="$1"
|
|
local name
|
|
|
|
config_get name "$section" name
|
|
[ -z "$name" ] && return
|
|
|
|
# Skip userlists already defined in AUTH_USERLIST_FILE to avoid duplicates
|
|
if [ -f "$AUTH_USERLIST_FILE" ] && grep -q "^userlist $name\$" "$AUTH_USERLIST_FILE" 2>/dev/null; then
|
|
return
|
|
fi
|
|
|
|
echo "userlist $name"
|
|
# Handle list of users
|
|
config_list_foreach "$section" user _print_userlist_user
|
|
echo ""
|
|
}
|
|
|
|
_print_userlist_user() {
|
|
local entry="$1"
|
|
local username="${entry%%:*}"
|
|
local password="${entry#*:}"
|
|
echo " user $username password $password"
|
|
}
|
|
|
|
# Generate all UCI-defined userlists
|
|
_generate_uci_userlists() {
|
|
config_load haproxy
|
|
config_foreach _print_uci_userlist userlist
|
|
}
|
|
|
|
generate_config() {
|
|
load_config
|
|
|
|
local cfg_file="$CONFIG_PATH/haproxy.cfg"
|
|
|
|
log_info "Generating HAProxy configuration..."
|
|
|
|
# Generate certs.list for multi-certificate SNI support
|
|
generate_certs_list
|
|
|
|
# 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
|
|
lua-load /opt/haproxy/lua/crowdsec.lua
|
|
|
|
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
|
|
|
|
# SecuBox userlist (for private vhosts)
|
|
if [ -f "$AUTH_USERLIST_FILE" ]; then
|
|
cat "$AUTH_USERLIST_FILE" >> "$cfg_file"
|
|
echo "" >> "$cfg_file"
|
|
fi
|
|
|
|
# Generate UCI-defined userlists
|
|
_generate_uci_userlists >> "$cfg_file"
|
|
|
|
# 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"
|
|
|
|
# Sync config to all locations HAProxy might read from
|
|
cp "$cfg_file" /etc/haproxy.cfg 2>/dev/null || true
|
|
}
|
|
|
|
_generate_frontends() {
|
|
# HTTP Frontend
|
|
cat << EOF
|
|
frontend http-in
|
|
bind *:$http_port,[::]:$http_port
|
|
mode http
|
|
|
|
# ACME challenge routing (no HAProxy restart needed for cert issuance)
|
|
acl is_acme_challenge path_beg /.well-known/acme-challenge/
|
|
use_backend acme_challenge if is_acme_challenge
|
|
|
|
# CrowdSec IP blocking (dual-layer WAF with mitmproxy)
|
|
http-request lua.crowdsec_check if !is_acme_challenge
|
|
http-request deny deny_status 403 if { var(txn.blocked) -m str 1 }
|
|
|
|
EOF
|
|
|
|
# Add HTTPS redirect rules for vhosts with ssl_redirect
|
|
config_load haproxy
|
|
config_foreach _add_ssl_redirect vhost
|
|
|
|
# Add path-based ACLs BEFORE vhost ACLs (path rules take precedence)
|
|
# Two-phase: collect then emit sorted by pattern length (longest first)
|
|
rm -f "$PATH_ACL_TMPFILE"
|
|
config_foreach _collect_path_acl acl
|
|
_emit_sorted_path_acls
|
|
|
|
# Add vhost ACLs for HTTP (specific domains first, then wildcards)
|
|
config_foreach _add_vhost_acl vhost "http" "exact"
|
|
config_foreach _add_vhost_acl vhost "http" "suffix"
|
|
|
|
echo " default_backend $default_backend"
|
|
echo ""
|
|
|
|
# HTTPS Frontend (if certificates exist)
|
|
# Use container path /opt/haproxy/certs/ (not host path)
|
|
local CONTAINER_CERTS_PATH="/opt/haproxy/certs"
|
|
local CERTS_LIST_FILE="$CERTS_PATH/certs.list"
|
|
if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
|
|
# Use crt-list for proper multi-certificate SNI matching
|
|
# Falls back to directory mode if certs.list doesn't exist
|
|
if [ -f "$CERTS_LIST_FILE" ] && [ -s "$CERTS_LIST_FILE" ]; then
|
|
cat << EOF
|
|
frontend https-in
|
|
bind *:$https_port,[::]:$https_port ssl crt-list $CONTAINER_CERTS_PATH/certs.list alpn http/1.1
|
|
mode http
|
|
http-request set-header X-Forwarded-Proto https
|
|
http-request set-header X-Real-IP %[src]
|
|
|
|
# Security: Block access to sensitive paths
|
|
acl is_sensitive_path path_beg /.git
|
|
acl is_sensitive_path path_beg /.svn
|
|
acl is_sensitive_path path_beg /.env
|
|
acl is_sensitive_path path_beg /.htaccess
|
|
acl is_sensitive_path path_beg /.htpasswd
|
|
http-request deny if is_sensitive_path
|
|
|
|
# CrowdSec IP blocking (dual-layer WAF with mitmproxy)
|
|
http-request lua.crowdsec_check
|
|
http-request deny deny_status 403 if { var(txn.blocked) -m str 1 }
|
|
EOF
|
|
else
|
|
# Fallback to directory mode if no certs.list
|
|
cat << EOF
|
|
frontend https-in
|
|
bind *:$https_port,[::]:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn http/1.1
|
|
mode http
|
|
http-request set-header X-Forwarded-Proto https
|
|
http-request set-header X-Real-IP %[src]
|
|
|
|
# Security: Block access to sensitive paths
|
|
acl is_sensitive_path path_beg /.git
|
|
acl is_sensitive_path path_beg /.svn
|
|
acl is_sensitive_path path_beg /.env
|
|
acl is_sensitive_path path_beg /.htaccess
|
|
acl is_sensitive_path path_beg /.htpasswd
|
|
http-request deny if is_sensitive_path
|
|
|
|
# CrowdSec IP blocking (dual-layer WAF with mitmproxy)
|
|
http-request lua.crowdsec_check
|
|
http-request deny deny_status 403 if { var(txn.blocked) -m str 1 }
|
|
EOF
|
|
fi
|
|
# Add path-based ACLs BEFORE vhost ACLs (path rules take precedence)
|
|
# Two-phase: collect then emit sorted by pattern length (longest first)
|
|
rm -f "$PATH_ACL_TMPFILE"
|
|
config_foreach _collect_path_acl acl
|
|
_emit_sorted_path_acls
|
|
|
|
# Add vhost ACLs for HTTPS (specific domains first, then wildcards)
|
|
config_foreach _add_vhost_acl vhost "https" "exact"
|
|
config_foreach _add_vhost_acl vhost "https" "suffix"
|
|
|
|
echo " default_backend $default_backend"
|
|
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 } !is_acme_challenge"
|
|
}
|
|
|
|
# Path ACL collection temp file
|
|
PATH_ACL_TMPFILE="/tmp/haproxy_path_acls.$$"
|
|
|
|
# Collect path-based ACLs from UCI 'acl' sections into temp file for sorting
|
|
# Format: pattern_length|section|type|pattern|backend|host
|
|
_collect_path_acl() {
|
|
local section="$1"
|
|
local enabled type pattern backend host priority
|
|
|
|
config_get enabled "$section" enabled "1"
|
|
[ "$enabled" = "1" ] || return
|
|
|
|
config_get type "$section" type
|
|
config_get pattern "$section" pattern
|
|
config_get backend "$section" backend
|
|
config_get host "$section" host
|
|
config_get priority "$section" priority "100"
|
|
|
|
# Validate required fields
|
|
[ -n "$type" ] || return
|
|
[ -n "$pattern" ] || return
|
|
[ -n "$backend" ] || return
|
|
|
|
# Calculate pattern length for sorting (longer patterns first)
|
|
local pattern_len=${#pattern}
|
|
|
|
# Write to temp file: length|section|type|pattern|backend|host
|
|
echo "${pattern_len}|${section}|${type}|${pattern}|${backend}|${host}" >> "$PATH_ACL_TMPFILE"
|
|
}
|
|
|
|
# Emit sorted path ACLs (longest patterns first for correct matching)
|
|
# This ensures /gk2/evolution matches before /gk2
|
|
_emit_sorted_path_acls() {
|
|
[ -f "$PATH_ACL_TMPFILE" ] || return
|
|
|
|
local seen_hosts=""
|
|
|
|
# Sort by pattern length (descending, numeric) and emit
|
|
sort -t'|' -k1 -rn "$PATH_ACL_TMPFILE" | while IFS='|' read -r len section type pattern backend host; do
|
|
local acl_name=$(echo "$section" | tr '.' '_' | tr '-' '_')
|
|
local host_acl_name=""
|
|
|
|
# If host is specified, emit host ACL only once
|
|
if [ -n "$host" ]; then
|
|
host_acl_name=$(echo "$host" | tr '.' '_' | tr '-' '_')
|
|
# Check if we've already emitted this host ACL
|
|
case "$seen_hosts" in
|
|
*"|$host_acl_name|"*) ;;
|
|
*)
|
|
echo " acl host_${host_acl_name} hdr(host) -i $host"
|
|
seen_hosts="${seen_hosts}|${host_acl_name}|"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Generate path ACL based on type
|
|
case "$type" in
|
|
path_beg)
|
|
echo " acl ${acl_name} path_beg $pattern"
|
|
;;
|
|
path_end)
|
|
echo " acl ${acl_name} path_end $pattern"
|
|
;;
|
|
path)
|
|
echo " acl ${acl_name} path $pattern"
|
|
;;
|
|
path_reg)
|
|
echo " acl ${acl_name} path_reg $pattern"
|
|
;;
|
|
path_dir)
|
|
echo " acl ${acl_name} path_dir $pattern"
|
|
;;
|
|
esac
|
|
|
|
# Generate use_backend rule (use WAF backend if enabled)
|
|
local effective_backend="$backend"
|
|
config_get waf_bypass "$section" waf_bypass "0"
|
|
[ "$waf_enabled" = "1" ] && [ "$waf_bypass" != "1" ] && effective_backend="$waf_backend"
|
|
|
|
# Set nocache flag during request for checking during response
|
|
# Note: http-request rules must come BEFORE use_backend
|
|
config_get no_cache "$section" no_cache "0"
|
|
if [ "$no_cache" = "1" ]; then
|
|
if [ -n "$host_acl_name" ]; then
|
|
echo " http-request set-var(txn.nocache) str(yes) if host_${host_acl_name} ${acl_name}"
|
|
else
|
|
echo " http-request set-var(txn.nocache) str(yes) if ${acl_name}"
|
|
fi
|
|
fi
|
|
|
|
if [ -n "$host_acl_name" ]; then
|
|
echo " use_backend $effective_backend if host_${host_acl_name} ${acl_name}"
|
|
else
|
|
echo " use_backend $effective_backend if ${acl_name}"
|
|
fi
|
|
done
|
|
|
|
rm -f "$PATH_ACL_TMPFILE"
|
|
}
|
|
|
|
_add_vhost_acl() {
|
|
local section="$1"
|
|
local proto="$2"
|
|
local filter="${3:-all}" # Filter: exact, suffix, regex, or all
|
|
local enabled domain backend ssl match_type
|
|
|
|
config_get enabled "$section" enabled "0"
|
|
[ "$enabled" = "1" ] || return
|
|
|
|
config_get domain "$section" domain
|
|
config_get backend "$section" backend
|
|
config_get match_type "$section" match_type "exact"
|
|
|
|
# Filter by match_type if specified (to process specific vhosts before wildcards)
|
|
if [ "$filter" != "all" ]; then
|
|
# For "exact" filter, also include regex (both are specific, not wildcard)
|
|
if [ "$filter" = "exact" ]; then
|
|
[ "$match_type" = "suffix" ] && return
|
|
elif [ "$filter" = "suffix" ]; then
|
|
[ "$match_type" != "suffix" ] && return
|
|
fi
|
|
fi
|
|
|
|
# Validate backend is not IP:port (common misconfiguration)
|
|
case "$backend" in
|
|
*:*) log_warn "Vhost $section has IP:port backend , should be backend name"; return ;;
|
|
esac
|
|
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 "-" "_" | tr "*" "wildcard")
|
|
|
|
# Handle different match types
|
|
case "$match_type" in
|
|
suffix)
|
|
# Suffix match for wildcard subdomains (e.g., .gk2.secubox.in)
|
|
echo " acl host_${acl_name} hdr(host) -m end -i $domain"
|
|
;;
|
|
regex)
|
|
# Regex match
|
|
echo " acl host_${acl_name} hdr(host) -m reg -i $domain"
|
|
;;
|
|
*)
|
|
# Exact match (default)
|
|
echo " acl host_${acl_name} hdr(host) -i $domain"
|
|
;;
|
|
esac
|
|
|
|
# Add auth requirement for private vhosts
|
|
local auth_enabled auth_realm auth_userlist
|
|
config_get auth_enabled "$section" auth_enabled "0"
|
|
if [ "$auth_enabled" = "1" ]; then
|
|
config_get auth_realm "$section" auth_realm "SecuBox"
|
|
config_get auth_userlist "$section" auth_userlist "secubox_users"
|
|
echo " http-request auth realm \"$auth_realm\" if host_${acl_name} !{ http_auth($auth_userlist) }"
|
|
# Log auth user in header for backend (http_auth_user only works after successful auth)
|
|
echo " http-request set-header X-Auth-User %[http_auth_user] if host_${acl_name}"
|
|
fi
|
|
|
|
# Use WAF backend if enabled, otherwise use original backend
|
|
local effective_backend="$backend"
|
|
config_get waf_bypass "$section" waf_bypass "0"
|
|
[ "$waf_enabled" = "1" ] && [ "$waf_bypass" != "1" ] && effective_backend="$waf_backend"
|
|
# Set nocache flag during request for checking during response
|
|
config_get no_cache "$section" no_cache "0"
|
|
if [ "$no_cache" = "1" ]; then
|
|
echo " http-request set-var(txn.nocache) str(yes) if host_${acl_name}"
|
|
fi
|
|
echo " use_backend $effective_backend if host_${acl_name}"
|
|
# Add no-cache headers if configured
|
|
config_get no_cache "$section" no_cache "0"
|
|
if [ "$no_cache" = "1" ]; then
|
|
echo " http-response set-header Cache-Control \"no-cache, no-store, must-revalidate\" if { var(txn.nocache) -m str yes }"
|
|
echo " http-response set-header Pragma \"no-cache\" if { var(txn.nocache) -m str yes }"
|
|
echo " http-response set-header Expires \"0\" if { var(txn.nocache) -m str yes }"
|
|
fi
|
|
|
|
}
|
|
|
|
_generate_backends() {
|
|
config_load haproxy
|
|
|
|
# ACME challenge backend (for certificate issuance without HAProxy restart)
|
|
# Serves /.well-known/acme-challenge/ from /var/www/acme-challenge/
|
|
cat << EOF
|
|
|
|
backend acme_challenge
|
|
mode http
|
|
server acme_webroot 192.168.255.1:8402 check
|
|
|
|
EOF
|
|
|
|
# Track which backends are generated
|
|
_generated_backends=""
|
|
|
|
# Generate each backend from UCI
|
|
config_foreach _generate_backend backend
|
|
|
|
# Collect all backends referenced by vhosts
|
|
_referenced_backends=""
|
|
_collect_vhost_backend() {
|
|
local section="$1"
|
|
local enabled backend
|
|
config_get enabled "$section" enabled "0"
|
|
[ "$enabled" = "1" ] || return
|
|
config_get backend "$section" backend
|
|
[ -n "$backend" ] && _referenced_backends="$_referenced_backends $backend"
|
|
}
|
|
config_foreach _collect_vhost_backend vhost
|
|
|
|
# Add default_backend to referenced list
|
|
_referenced_backends="$_referenced_backends $default_backend"
|
|
|
|
# Generate fallback backends for any referenced but not generated
|
|
# These common backends route to uhttpd on the host
|
|
for backend_name in default_luci luci apps; do
|
|
# Check if this backend is referenced
|
|
echo "$_referenced_backends" | grep -qw "$backend_name" || continue
|
|
# Check if already generated
|
|
echo "$_generated_backends" | grep -qw "$backend_name" && continue
|
|
# Generate fallback
|
|
cat << EOF
|
|
|
|
backend $backend_name
|
|
mode http
|
|
balance roundrobin
|
|
server $backend_name 192.168.255.1:8081 check
|
|
EOF
|
|
done
|
|
}
|
|
|
|
_generate_backend() {
|
|
local section="$1"
|
|
local enabled name mode balance health_check health_check_uri http_request
|
|
|
|
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 ""
|
|
config_get health_check_uri "$section" health_check_uri ""
|
|
|
|
# Track generated backend
|
|
_generated_backends="$_generated_backends $name"
|
|
|
|
echo ""
|
|
echo "backend $name"
|
|
echo " mode $mode"
|
|
|
|
# Check for http-request directives (always as list)
|
|
local has_http_request_return=0
|
|
local has_http_requests=0
|
|
_emit_and_check_http_request() {
|
|
local val="$1"
|
|
has_http_requests=1
|
|
echo " http-request $val"
|
|
case "$val" in
|
|
return*) has_http_request_return=1 ;;
|
|
esac
|
|
}
|
|
config_list_foreach "$section" http_request _emit_and_check_http_request
|
|
|
|
# If any http-request was a "return" directive, skip servers
|
|
[ "$has_http_request_return" = "1" ] && return
|
|
|
|
echo " balance $balance"
|
|
|
|
# Health check configuration
|
|
if [ -n "$health_check" ]; then
|
|
echo " option $health_check"
|
|
# If health_check_uri specified, use HTTP check with GET method
|
|
if [ -n "$health_check_uri" ]; then
|
|
echo " http-check send meth GET uri $health_check_uri"
|
|
echo " http-check expect status 200"
|
|
fi
|
|
fi
|
|
|
|
# Check if there are separate server sections for this backend
|
|
local has_server_sections=0
|
|
_check_server_sections() {
|
|
local srv_section="$1"
|
|
local srv_backend
|
|
config_get srv_backend "$srv_section" backend
|
|
config_get srv_enabled "$srv_section" enabled "0"
|
|
if [ "$srv_backend" = "$name" ] && [ "$srv_enabled" = "1" ]; then
|
|
has_server_sections=1
|
|
fi
|
|
}
|
|
config_foreach _check_server_sections server
|
|
|
|
# Add inline server ONLY if no separate server sections exist
|
|
# This prevents duplicate server names
|
|
if [ "$has_server_sections" = "0" ]; then
|
|
local server_line
|
|
config_get server_line "$section" server ""
|
|
[ -n "$server_line" ] && echo " server $server_line"
|
|
fi
|
|
|
|
# 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
|
|
|
|
# Validate address - if it's a hostname (not IP), try to resolve it
|
|
if ! echo "$address" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
# It's a hostname, try to resolve it
|
|
local resolved_ip=""
|
|
resolved_ip=$(nslookup "$address" 2>/dev/null | awk '/^Address: / { print $2; exit }')
|
|
if [ -z "$resolved_ip" ]; then
|
|
# Try getent as fallback
|
|
resolved_ip=$(getent hosts "$address" 2>/dev/null | awk '{print $1; exit}')
|
|
fi
|
|
if [ -z "$resolved_ip" ]; then
|
|
log_warn "Cannot resolve hostname '$address' for server $server_name in backend $target_backend - skipping"
|
|
return
|
|
fi
|
|
# Use the resolved IP instead
|
|
log_debug "Resolved $address to $resolved_ip"
|
|
address="$resolved_ip"
|
|
fi
|
|
|
|
local check_opt=""
|
|
[ "$check" = "1" ] && check_opt="check"
|
|
|
|
echo " server $server_name $address:$port weight $weight $check_opt"
|
|
}
|
|
|
|
# ===========================================
|
|
# Certificate Management
|
|
# ===========================================
|
|
|
|
# Check if certificate is from Let's Encrypt Production (not Staging)
|
|
cert_is_production() {
|
|
local cert_file="$1"
|
|
[ -f "$cert_file" ] || return 1
|
|
|
|
# Check the issuer - staging certs have "(STAGING)" in the issuer
|
|
local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null)
|
|
if echo "$issuer" | grep -qi "staging\|test\|fake"; then
|
|
return 1 # Staging certificate
|
|
fi
|
|
|
|
# Check for Let's Encrypt production issuers
|
|
if echo "$issuer" | grep -qiE "Let's Encrypt|R3|R10|R11|E1|E2|ISRG"; then
|
|
return 0 # Production certificate
|
|
fi
|
|
|
|
# Check if it's a self-signed or other CA
|
|
return 0 # Assume production for other CAs
|
|
}
|
|
|
|
# Validate certificate publicly using external service
|
|
cert_validate_public() {
|
|
local domain="$1"
|
|
local timeout=10
|
|
|
|
# Try to connect and verify the certificate
|
|
if command -v curl >/dev/null 2>&1; then
|
|
if curl -sS --max-time "$timeout" -o /dev/null "https://$domain" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Fallback: use openssl s_client
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
local result=$(echo | timeout "$timeout" openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -dates 2>/dev/null)
|
|
if [ -n "$result" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Get certificate info
|
|
cert_info() {
|
|
local cert_file="$1"
|
|
[ -f "$cert_file" ] || return 1
|
|
|
|
local subject=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | sed 's/subject=//')
|
|
local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | sed 's/issuer=//')
|
|
local not_after=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
|
|
local not_before=$(openssl x509 -in "$cert_file" -noout -startdate 2>/dev/null | cut -d= -f2)
|
|
|
|
echo "Subject: $subject"
|
|
echo "Issuer: $issuer"
|
|
echo "Valid From: $not_before"
|
|
echo "Valid Until: $not_after"
|
|
|
|
if cert_is_production "$cert_file"; then
|
|
echo "Type: PRODUCTION (publicly trusted)"
|
|
else
|
|
echo "Type: STAGING/TEST (NOT publicly trusted!)"
|
|
fi
|
|
}
|
|
|
|
# Verify and report certificate status
|
|
cmd_cert_verify() {
|
|
load_config
|
|
|
|
local domain="$1"
|
|
if [ -z "$domain" ]; then
|
|
echo "Usage: haproxyctl cert verify <domain>"
|
|
return 1
|
|
fi
|
|
|
|
local cert_file="$CERTS_PATH/$domain.pem"
|
|
if [ ! -f "$cert_file" ]; then
|
|
log_error "Certificate not found: $cert_file"
|
|
return 1
|
|
fi
|
|
|
|
echo "Certificate Information for $domain:"
|
|
echo "======================================"
|
|
cert_info "$cert_file"
|
|
echo ""
|
|
|
|
# Check if it's production
|
|
if ! cert_is_production "$cert_file"; then
|
|
log_warn "This is a STAGING certificate - NOT trusted by browsers!"
|
|
log_warn "To get a production certificate, ensure staging='0' in config and re-issue"
|
|
return 1
|
|
fi
|
|
|
|
# Try public validation
|
|
echo "Public Validation:"
|
|
if cert_validate_public "$domain"; then
|
|
log_info "Certificate is publicly valid and accessible"
|
|
return 0
|
|
else
|
|
log_warn "Could not verify certificate publicly"
|
|
log_warn "Ensure DNS points to this server and port 443 is accessible"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
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)
|
|
local type_icon="✅"
|
|
if ! cert_is_production "$cert"; then
|
|
type_icon="⚠️ STAGING"
|
|
fi
|
|
echo " $name - Expires: ${expiry:-Unknown} $type_icon"
|
|
done
|
|
else
|
|
echo " No certificates found"
|
|
fi
|
|
|
|
# Show current mode
|
|
local staging=$(uci -q get haproxy.acme.staging)
|
|
echo ""
|
|
if [ "$staging" = "1" ]; then
|
|
echo "⚠️ ACME Mode: STAGING (certificates will NOT be trusted by browsers)"
|
|
echo " To use production: uci set haproxy.acme.staging='0' && uci commit haproxy"
|
|
else
|
|
echo "✅ ACME Mode: PRODUCTION (certificates will be publicly trusted)"
|
|
fi
|
|
}
|
|
|
|
# Check if we have local BIND for a zone
|
|
has_local_bind_zone() {
|
|
local domain="$1"
|
|
local zone_file="/etc/bind/zones/${domain}.zone"
|
|
[ -f "$zone_file" ] && command -v rndc >/dev/null 2>&1
|
|
}
|
|
|
|
# Add TXT record to local BIND zone
|
|
bind_add_txt() {
|
|
local subdomain="$1"
|
|
local zone="$2"
|
|
local value="$3"
|
|
local zone_file="/etc/bind/zones/${zone}.zone"
|
|
|
|
[ ! -f "$zone_file" ] && return 1
|
|
|
|
# Update serial (format: YYYYMMDDNN)
|
|
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
|
local today=$(date +%Y%m%d)
|
|
local new_serial
|
|
|
|
if [ -n "$current_serial" ]; then
|
|
local serial_date="${current_serial:0:8}"
|
|
local serial_num="${current_serial:8:2}"
|
|
if [ "$serial_date" = "$today" ]; then
|
|
serial_num=$((10#$serial_num + 1))
|
|
new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
|
else
|
|
new_serial="${today}01"
|
|
fi
|
|
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
|
fi
|
|
|
|
# Add TXT record (before the ; IPv6 comment or at end)
|
|
local record="$subdomain IN TXT \"$value\""
|
|
if grep -q "; IPv6" "$zone_file"; then
|
|
sed -i "/; IPv6/i $record" "$zone_file"
|
|
else
|
|
echo "$record" >> "$zone_file"
|
|
fi
|
|
|
|
# Reload BIND
|
|
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null
|
|
log_info "Added TXT record: $subdomain.$zone -> $value"
|
|
}
|
|
|
|
# Remove TXT record from local BIND zone
|
|
bind_remove_txt() {
|
|
local subdomain="$1"
|
|
local zone="$2"
|
|
local value="$3"
|
|
local zone_file="/etc/bind/zones/${zone}.zone"
|
|
|
|
[ ! -f "$zone_file" ] && return 1
|
|
|
|
# Remove the specific TXT record
|
|
sed -i "/$subdomain IN TXT \"$value\"/d" "$zone_file"
|
|
|
|
# Update serial
|
|
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
|
if [ -n "$current_serial" ]; then
|
|
local today=$(date +%Y%m%d)
|
|
local serial_date="${current_serial:0:8}"
|
|
local serial_num="${current_serial:8:2}"
|
|
if [ "$serial_date" = "$today" ]; then
|
|
serial_num=$((10#$serial_num + 1))
|
|
else
|
|
serial_num=1
|
|
fi
|
|
local new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
|
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
|
fi
|
|
|
|
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null
|
|
log_info "Removed TXT record: $subdomain.$zone"
|
|
}
|
|
|
|
# Get DNS provider configuration for DNS-01 challenge
|
|
get_dns_provider_env() {
|
|
local provider=$(uci -q get dns-provider.main.provider)
|
|
[ -z "$provider" ] && return 1
|
|
|
|
case "$provider" in
|
|
local|bind)
|
|
# Using local BIND - acme.sh will use our custom hook
|
|
echo "dns_local_bind"
|
|
;;
|
|
gandi)
|
|
local api_key=$(uci -q get dns-provider.gandi.api_key)
|
|
[ -z "$api_key" ] && return 1
|
|
export GANDI_LIVEDNS_KEY="$api_key"
|
|
echo "dns_gandi_livedns"
|
|
;;
|
|
ovh)
|
|
local app_key=$(uci -q get dns-provider.ovh.app_key)
|
|
local app_secret=$(uci -q get dns-provider.ovh.app_secret)
|
|
local consumer_key=$(uci -q get dns-provider.ovh.consumer_key)
|
|
local endpoint=$(uci -q get dns-provider.ovh.endpoint)
|
|
[ -z "$app_key" ] || [ -z "$app_secret" ] || [ -z "$consumer_key" ] && return 1
|
|
export OVH_AK="$app_key"
|
|
export OVH_AS="$app_secret"
|
|
export OVH_CK="$consumer_key"
|
|
export OVH_END="${endpoint:-ovh-eu}"
|
|
echo "dns_ovh"
|
|
;;
|
|
cloudflare)
|
|
local api_token=$(uci -q get dns-provider.cloudflare.api_token)
|
|
[ -z "$api_token" ] && return 1
|
|
export CF_Token="$api_token"
|
|
echo "dns_cloudflare"
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# Create acme.sh DNS hook for local BIND
|
|
create_local_dns_hook() {
|
|
local hook_dir="/etc/acme/dnsapi"
|
|
ensure_dir "$hook_dir"
|
|
|
|
cat > "$hook_dir/dns_local_bind.sh" << 'HOOK'
|
|
#!/bin/sh
|
|
# acme.sh DNS hook for local BIND zones
|
|
|
|
dns_local_bind_add() {
|
|
local fulldomain="$1"
|
|
local txtvalue="$2"
|
|
|
|
# Extract zone from domain (e.g., _acme-challenge.gk2.secubox.in -> secubox.in)
|
|
local zone=$(echo "$fulldomain" | awk -F. '{print $(NF-1)"."$NF}')
|
|
local subdomain="${fulldomain%.$zone}"
|
|
|
|
local zone_file="/etc/bind/zones/${zone}.zone"
|
|
[ ! -f "$zone_file" ] && { echo "Zone file not found: $zone_file"; return 1; }
|
|
|
|
# Update serial
|
|
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
|
if [ -n "$current_serial" ]; then
|
|
local today=$(date +%Y%m%d)
|
|
local serial_date="${current_serial:0:8}"
|
|
local serial_num="${current_serial:8:2}"
|
|
if [ "$serial_date" = "$today" ]; then
|
|
serial_num=$((10#$serial_num + 1))
|
|
else
|
|
serial_num=1
|
|
fi
|
|
local new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
|
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
|
fi
|
|
|
|
# Add TXT record
|
|
local record="$subdomain IN TXT \"$txtvalue\""
|
|
if grep -q "; IPv6" "$zone_file"; then
|
|
sed -i "/; IPv6/i $record" "$zone_file"
|
|
else
|
|
echo "$record" >> "$zone_file"
|
|
fi
|
|
|
|
# Reload BIND (use kill -HUP since rndc may not be configured)
|
|
local bind_pid=$(pidof named 2>/dev/null)
|
|
if [ -n "$bind_pid" ]; then
|
|
kill -HUP "$bind_pid" 2>/dev/null
|
|
fi
|
|
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null || true
|
|
echo "Added: $fulldomain TXT $txtvalue"
|
|
|
|
# Wait for DNS propagation
|
|
sleep 5
|
|
return 0
|
|
}
|
|
|
|
dns_local_bind_rm() {
|
|
local fulldomain="$1"
|
|
local txtvalue="$2"
|
|
|
|
local zone=$(echo "$fulldomain" | awk -F. '{print $(NF-1)"."$NF}')
|
|
local subdomain="${fulldomain%.$zone}"
|
|
local zone_file="/etc/bind/zones/${zone}.zone"
|
|
|
|
[ ! -f "$zone_file" ] && return 0
|
|
|
|
# Remove the TXT record
|
|
sed -i "/$subdomain IN TXT \"$txtvalue\"/d" "$zone_file"
|
|
|
|
# Update serial
|
|
local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
|
|
if [ -n "$current_serial" ]; then
|
|
local today=$(date +%Y%m%d)
|
|
local serial_date="${current_serial:0:8}"
|
|
local serial_num="${current_serial:8:2}"
|
|
if [ "$serial_date" = "$today" ]; then
|
|
serial_num=$((10#$serial_num + 1))
|
|
else
|
|
serial_num=1
|
|
fi
|
|
local new_serial=$(printf "%s%02d" "$today" "$serial_num")
|
|
sed -i "s/$current_serial/$new_serial/" "$zone_file"
|
|
fi
|
|
|
|
# Reload BIND
|
|
local bind_pid=$(pidof named 2>/dev/null)
|
|
if [ -n "$bind_pid" ]; then
|
|
kill -HUP "$bind_pid" 2>/dev/null
|
|
fi
|
|
rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null || true
|
|
echo "Removed: $fulldomain TXT $txtvalue"
|
|
return 0
|
|
}
|
|
HOOK
|
|
chmod +x "$hook_dir/dns_local_bind.sh"
|
|
}
|
|
|
|
cmd_cert_add() {
|
|
require_root
|
|
load_config
|
|
|
|
local domain="$1"
|
|
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
|
|
|
|
# Detect if this is a wildcard certificate request
|
|
local is_wildcard=0
|
|
local base_domain="$domain"
|
|
case "$domain" in
|
|
\*.*)
|
|
is_wildcard=1
|
|
base_domain="${domain#\*.}"
|
|
log_info "Wildcard certificate requested for $domain"
|
|
;;
|
|
esac
|
|
|
|
# Ensure HAProxy config has ACME backend (for webroot mode)
|
|
log_info "Ensuring HAProxy config is up to date..."
|
|
generate_config
|
|
|
|
# Ensure firewall allows HTTP/HTTPS from WAN (required for ACME)
|
|
log_info "Checking firewall rules..."
|
|
firewall_ensure_haproxy_rules "$http_port" "$https_port"
|
|
|
|
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; }
|
|
|
|
# For wildcard certs, DNS-01 challenge is REQUIRED
|
|
local dns_plugin=""
|
|
local use_local_bind=0
|
|
if [ "$is_wildcard" = "1" ]; then
|
|
# First check if we have local BIND for this zone
|
|
# Extract parent zone from domain (e.g., *.gk2.secubox.in -> secubox.in)
|
|
# Use awk instead of rev which isn't available on OpenWrt
|
|
local parent_zone=$(echo "$base_domain" | awk -F. '{print $(NF-1)"."$NF}')
|
|
if has_local_bind_zone "$parent_zone"; then
|
|
log_info "Local BIND zone found for $parent_zone"
|
|
use_local_bind=1
|
|
dns_plugin="dns_local_bind"
|
|
create_local_dns_hook
|
|
else
|
|
# Try external DNS provider
|
|
dns_plugin=$(get_dns_provider_env)
|
|
fi
|
|
|
|
if [ -z "$dns_plugin" ]; then
|
|
log_error "DNS provider not configured. Wildcard certificates require DNS-01 challenge."
|
|
log_error "Options:"
|
|
log_error " 1. Use local BIND: ensure /etc/bind/zones/<zone>.zone exists"
|
|
log_error " 2. Configure external DNS: LuCI > Services > DNS Provider"
|
|
log_error " uci set dns-provider.main.provider='gandi'"
|
|
log_error " uci set dns-provider.gandi.api_key='YOUR_API_KEY'"
|
|
log_error " uci commit dns-provider"
|
|
return 1
|
|
fi
|
|
log_info "Using DNS-01 challenge with $dns_plugin"
|
|
fi
|
|
|
|
# Warn about staging mode
|
|
if [ "$staging" = "1" ]; then
|
|
log_warn "=========================================="
|
|
log_warn "STAGING MODE ENABLED!"
|
|
log_warn "Certificate will NOT be trusted by browsers"
|
|
log_warn "To use production: uci set haproxy.acme.staging='0' && uci commit haproxy"
|
|
log_warn "=========================================="
|
|
sleep 2
|
|
else
|
|
log_info "Using Let's Encrypt PRODUCTION (certificates will be publicly trusted)"
|
|
fi
|
|
|
|
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" --server letsencrypt $staging_flag --home "$LE_WORKING_DIR" || true
|
|
fi
|
|
|
|
local acme_result=0
|
|
|
|
if [ "$is_wildcard" = "1" ]; then
|
|
# DNS-01 challenge for wildcard certificates
|
|
log_info "Issuing wildcard certificate via DNS-01 challenge..."
|
|
log_info "This will create a TXT record at _acme-challenge.$base_domain"
|
|
|
|
"$ACME_SH" --issue -d "$domain" -d "$base_domain" \
|
|
--server letsencrypt \
|
|
--dns "$dns_plugin" \
|
|
--keylength "$key_type" \
|
|
$staging_flag \
|
|
--home "$LE_WORKING_DIR" || acme_result=$?
|
|
else
|
|
# HTTP-01 challenge for regular domains (webroot mode)
|
|
# Setup webroot for ACME challenges (HAProxy routes /.well-known/acme-challenge/ here)
|
|
local ACME_WEBROOT="/var/www/acme-challenge"
|
|
ensure_dir "$ACME_WEBROOT/.well-known/acme-challenge"
|
|
chmod 755 "$ACME_WEBROOT" "$ACME_WEBROOT/.well-known" "$ACME_WEBROOT/.well-known/acme-challenge"
|
|
|
|
# Start simple webserver for ACME challenges (if not already running)
|
|
local ACME_PORT=8402
|
|
if ! netstat -tln 2>/dev/null | grep -q ":$ACME_PORT "; then
|
|
log_info "Starting ACME challenge webserver on port $ACME_PORT..."
|
|
# Use busybox httpd (available on OpenWrt)
|
|
start-stop-daemon -S -b -x /usr/sbin/httpd -- -p $ACME_PORT -h "$ACME_WEBROOT" -f 2>/dev/null || \
|
|
busybox httpd -p $ACME_PORT -h "$ACME_WEBROOT" &
|
|
sleep 1
|
|
fi
|
|
|
|
# Ensure HAProxy container is running with ACME backend
|
|
if ! lxc_running; then
|
|
lxc_start_bg || true
|
|
fi
|
|
|
|
# Issue certificate using webroot mode (NO HAProxy restart needed!)
|
|
log_info "Issuing certificate (webroot mode - HAProxy stays running)..."
|
|
"$ACME_SH" --issue -d "$domain" \
|
|
--server letsencrypt \
|
|
--webroot "$ACME_WEBROOT" \
|
|
--keylength "$key_type" \
|
|
$staging_flag \
|
|
--home "$LE_WORKING_DIR" || acme_result=$?
|
|
fi
|
|
|
|
# acme.sh returns 0 on success, 2 on "skip/already valid" - both are OK
|
|
# Install the certificate to our certs path
|
|
# For wildcard, use sanitized filename (replace * with _wildcard_)
|
|
local cert_filename="$domain"
|
|
case "$domain" in
|
|
\*.*) cert_filename="_wildcard_.${domain#\*.}" ;;
|
|
esac
|
|
|
|
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/$cert_filename.crt" \
|
|
--key-file "$CERTS_PATH/$cert_filename.key" \
|
|
--fullchain-file "$CERTS_PATH/$cert_filename.fullchain.pem" \
|
|
--reloadcmd "/usr/sbin/haproxyctl reload" 2>/dev/null || true
|
|
|
|
# HAProxy needs combined file: fullchain + private key
|
|
log_info "Creating combined PEM for HAProxy..."
|
|
cat "$CERTS_PATH/$cert_filename.fullchain.pem" "$CERTS_PATH/$cert_filename.key" > "$CERTS_PATH/$cert_filename.pem"
|
|
chmod 600 "$CERTS_PATH/$cert_filename.pem"
|
|
|
|
# Clean up intermediate files - HAProxy only needs the .pem file
|
|
rm -f "$CERTS_PATH/$cert_filename.crt" "$CERTS_PATH/$cert_filename.key" "$CERTS_PATH/$cert_filename.fullchain.pem" "$CERTS_PATH/$cert_filename.crt.key" 2>/dev/null
|
|
|
|
# Reload HAProxy to pick up new cert
|
|
log_info "Reloading HAProxy to use new certificate..."
|
|
lxc_reload
|
|
fi
|
|
|
|
# Check if certificate was created
|
|
if [ ! -f "$CERTS_PATH/$cert_filename.pem" ]; then
|
|
if [ "$is_wildcard" = "1" ]; then
|
|
log_error "Wildcard certificate issuance failed. Check:"
|
|
log_error " 1. DNS provider API credentials are correct"
|
|
log_error " 2. You have permission to modify DNS for $base_domain"
|
|
log_error " 3. DNS propagation may take time (try again in a few minutes)"
|
|
else
|
|
log_error "Certificate issuance failed. Check:"
|
|
log_error " 1. Domain $domain points to this server's public IP"
|
|
log_error " 2. Port 80 is accessible from internet"
|
|
log_error " 3. HAProxy is running with ACME backend (haproxyctl generate)"
|
|
fi
|
|
return 1
|
|
fi
|
|
log_info "Certificate ready: $CERTS_PATH/$cert_filename.pem"
|
|
|
|
# Update domain variable for UCI storage
|
|
domain="$cert_filename"
|
|
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"
|
|
|
|
# Verify certificate type (production vs staging)
|
|
echo ""
|
|
if cert_is_production "$CERTS_PATH/$domain.pem"; then
|
|
log_info "✅ Certificate is from PRODUCTION CA (publicly trusted)"
|
|
else
|
|
log_warn "⚠️ Certificate is from STAGING CA (NOT publicly trusted!)"
|
|
log_warn " Browsers will show security warnings for this certificate"
|
|
log_warn " To get a production certificate:"
|
|
log_warn " 1. uci set haproxy.acme.staging='0'"
|
|
log_warn " 2. uci commit haproxy"
|
|
log_warn " 3. haproxyctl cert remove $domain"
|
|
log_warn " 4. haproxyctl cert add $domain"
|
|
fi
|
|
|
|
# Show certificate info
|
|
echo ""
|
|
cert_info "$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"
|
|
|
|
# Offer to verify publicly if production
|
|
if cert_is_production "$CERTS_PATH/$domain.pem"; then
|
|
echo ""
|
|
log_info "To verify the certificate is working publicly, run:"
|
|
log_info " haproxyctl cert verify $domain"
|
|
fi
|
|
}
|
|
|
|
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"
|
|
local nowaf="$3"
|
|
|
|
[ -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"
|
|
|
|
# Route through WAF (mitmproxy_inspector) by default unless --nowaf specified
|
|
# Store original backend for mitmproxy route resolution
|
|
if [ "$nowaf" != "--nowaf" ] && [ "$backend" != "mitmproxy_inspector" ] && [ "$backend" != "fallback" ]; then
|
|
uci set haproxy.$section.backend="mitmproxy_inspector"
|
|
uci set haproxy.$section.original_backend="$backend"
|
|
log_info "WAF protection enabled: $domain -> mitmproxy_inspector -> $backend"
|
|
else
|
|
uci set haproxy.$section.backend="$backend"
|
|
fi
|
|
|
|
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"
|
|
|
|
# Auto-sync mitmproxy routes to ensure WAF routing works
|
|
if [ -x /usr/sbin/mitmproxyctl ]; then
|
|
log_info "Syncing mitmproxy routes..."
|
|
/usr/sbin/mitmproxyctl sync-routes >/dev/null 2>&1 &
|
|
fi
|
|
|
|
# Regenerate GK2 Hub landing page if generator exists
|
|
[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
|
|
}
|
|
|
|
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"
|
|
|
|
# Auto-sync mitmproxy routes to clean up orphaned routes
|
|
if [ -x /usr/sbin/mitmproxyctl ]; then
|
|
log_info "Syncing mitmproxy routes..."
|
|
/usr/sbin/mitmproxyctl sync-routes >/dev/null 2>&1 &
|
|
fi
|
|
|
|
# Regenerate GK2 Hub landing page if generator exists
|
|
[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
|
|
}
|
|
|
|
# ===========================================
|
|
# 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: haproxyctl service-run (foreground) or lxc-start -n haproxy -d (background)"
|
|
}
|
|
|
|
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..."
|
|
# Copy generated config to container's /etc/haproxy/ (HAProxy reads from there)
|
|
lxc_exec cp /opt/haproxy/config/haproxy.cfg /etc/haproxy/haproxy.cfg
|
|
# 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"
|
|
|
|
# Regenerate GK2 Hub landing page if generator exists
|
|
[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
# ===========================================
|
|
# Path ACL Management
|
|
# ===========================================
|
|
|
|
cmd_path_list() {
|
|
config_load haproxy
|
|
echo "Path ACLs:"
|
|
echo "=========================================="
|
|
printf "%-20s %-25s %-25s %s\n" "NAME" "PATTERN" "BACKEND" "HOST"
|
|
echo "------------------------------------------"
|
|
config_foreach _print_path_acl acl
|
|
}
|
|
|
|
_print_path_acl() {
|
|
local section="$1"
|
|
local enabled pattern backend host type
|
|
config_get enabled "$section" enabled "1"
|
|
[ "$enabled" = "1" ] || return
|
|
config_get type "$section" type
|
|
config_get pattern "$section" pattern
|
|
config_get backend "$section" backend
|
|
config_get host "$section" host
|
|
[ -n "$pattern" ] || return
|
|
printf "%-20s %-25s %-25s %s\n" "$section" "$pattern" "$backend" "$host"
|
|
}
|
|
|
|
cmd_path_sync() {
|
|
local prefix="${1:-/gk2}"
|
|
local host="${2:-secubox.in}"
|
|
|
|
log_info "Syncing path ACLs: prefix=$prefix host=$host"
|
|
|
|
# Collect backend names
|
|
local backends=""
|
|
config_load haproxy
|
|
|
|
_collect_backend_name() {
|
|
local section="$1"
|
|
local name enabled
|
|
config_get name "$section" name "$section"
|
|
config_get enabled "$section" enabled "1"
|
|
[ "$enabled" = "1" ] || return
|
|
|
|
# Skip system backends
|
|
case "$name" in
|
|
fallback|acme_challenge|end_of_internet|vortex_*|luci_default) return ;;
|
|
esac
|
|
|
|
backends="$backends $name"
|
|
}
|
|
|
|
config_foreach _collect_backend_name backend
|
|
|
|
log_info "Found backends: $backends"
|
|
|
|
# Generate path ACL for each backend
|
|
local added=0
|
|
for backend in $backends; do
|
|
# Extract short name from backend (e.g., metablog_gk2 -> gk2, streamlit_evolution -> evolution)
|
|
local short_name=""
|
|
case "$backend" in
|
|
metablog_*) short_name="${backend#metablog_}" ;;
|
|
streamlit_*) short_name="${backend#streamlit_}" ;;
|
|
jellyfin_*) short_name="${backend#jellyfin_}" ;;
|
|
*) short_name="$backend" ;;
|
|
esac
|
|
|
|
local acl_name="path_$(echo "${prefix#/}" | tr '/' '_')_${short_name}"
|
|
local pattern="${prefix}/${short_name}"
|
|
|
|
# Check if ACL already exists
|
|
if uci -q get haproxy.${acl_name} >/dev/null 2>&1; then
|
|
log_debug "ACL exists: $acl_name"
|
|
continue
|
|
fi
|
|
|
|
log_info "Adding: $pattern -> $backend"
|
|
uci set haproxy.${acl_name}=acl
|
|
uci set haproxy.${acl_name}.type="path_beg"
|
|
uci set haproxy.${acl_name}.pattern="$pattern"
|
|
uci set haproxy.${acl_name}.backend="$backend"
|
|
uci set haproxy.${acl_name}.host="$host"
|
|
uci set haproxy.${acl_name}.enabled="1"
|
|
added=$((added + 1))
|
|
done
|
|
|
|
uci commit haproxy
|
|
log_info "Added $added new path ACLs"
|
|
|
|
# Regenerate and reload
|
|
if [ "$added" -gt 0 ]; then
|
|
generate_config
|
|
cmd_reload
|
|
fi
|
|
}
|
|
|
|
cmd_path_add() {
|
|
local pattern="$1"
|
|
local backend="$2"
|
|
local host="${3:-secubox.in}"
|
|
|
|
[ -n "$pattern" ] || { log_error "Pattern required"; return 1; }
|
|
[ -n "$backend" ] || { log_error "Backend required"; return 1; }
|
|
|
|
local acl_name="path_$(echo "${pattern#/}" | tr '/' '_' | tr '-' '_')"
|
|
|
|
log_info "Adding path ACL: $pattern -> $backend (host: $host)"
|
|
|
|
uci set haproxy.${acl_name}=acl
|
|
uci set haproxy.${acl_name}.type="path_beg"
|
|
uci set haproxy.${acl_name}.pattern="$pattern"
|
|
uci set haproxy.${acl_name}.backend="$backend"
|
|
uci set haproxy.${acl_name}.host="$host"
|
|
uci set haproxy.${acl_name}.enabled="1"
|
|
uci commit haproxy
|
|
|
|
generate_config
|
|
cmd_reload
|
|
}
|
|
|
|
cmd_path_remove() {
|
|
local name="$1"
|
|
[ -n "$name" ] || { log_error "ACL name required"; return 1; }
|
|
|
|
if uci -q get haproxy.${name} >/dev/null 2>&1; then
|
|
uci delete haproxy.${name}
|
|
uci commit haproxy
|
|
log_info "Removed path ACL: $name"
|
|
generate_config
|
|
cmd_reload
|
|
else
|
|
log_error "Path ACL not found: $name"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
cmd_path_clear() {
|
|
local prefix="$1"
|
|
[ -n "$prefix" ] || { log_error "Prefix required (e.g., /gk2)"; return 1; }
|
|
|
|
local acl_prefix="path_$(echo "${prefix#/}" | tr '/' '_')"
|
|
local removed=0
|
|
|
|
config_load haproxy
|
|
|
|
_check_and_remove_acl() {
|
|
local section="$1"
|
|
case "$section" in
|
|
${acl_prefix}_*)
|
|
uci delete haproxy.${section}
|
|
removed=$((removed + 1))
|
|
log_info "Removed: $section"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
config_foreach _check_and_remove_acl acl
|
|
|
|
if [ "$removed" -gt 0 ]; then
|
|
uci commit haproxy
|
|
log_info "Removed $removed path ACLs with prefix $prefix"
|
|
generate_config
|
|
cmd_reload
|
|
else
|
|
log_info "No path ACLs found with prefix $prefix"
|
|
fi
|
|
}
|
|
|
|
# ===========================================
|
|
# Authentication Management
|
|
# ===========================================
|
|
|
|
AUTH_USERLIST_FILE="$CONFIG_PATH/secubox-users.cfg"
|
|
|
|
# Sync SecuBox/LuCI users to HAProxy userlist
|
|
cmd_auth_sync() {
|
|
log_info "Syncing SecuBox users to HAProxy userlist..."
|
|
|
|
local tmpfile="/tmp/haproxy_users.$$"
|
|
local count=0
|
|
|
|
echo "# SecuBox Users - Auto-generated" > "$tmpfile"
|
|
echo "# Do not edit - regenerated by haproxyctl auth sync" >> "$tmpfile"
|
|
echo "" >> "$tmpfile"
|
|
echo "userlist secubox_users" >> "$tmpfile"
|
|
|
|
# Iterate through rpcd login users
|
|
for section in $(uci show rpcd 2>/dev/null | grep '=login' | cut -d'.' -f2 | cut -d'=' -f1); do
|
|
local username=$(uci -q get rpcd.${section}.username)
|
|
[ -z "$username" ] && continue
|
|
|
|
local pw_ref=$(uci -q get rpcd.${section}.password)
|
|
local hash=""
|
|
|
|
# Handle $p$username reference to /etc/shadow
|
|
if echo "$pw_ref" | grep -q '^\$p\$'; then
|
|
local sys_user="${pw_ref#\$p\$}"
|
|
local shadow_hash=$(grep "^${sys_user}:" /etc/shadow 2>/dev/null | cut -d: -f2)
|
|
if [ -n "$shadow_hash" ] && [ "$shadow_hash" != "*" ] && [ "$shadow_hash" != "x" ]; then
|
|
hash="$shadow_hash"
|
|
fi
|
|
else
|
|
hash="$pw_ref"
|
|
fi
|
|
|
|
if [ -n "$hash" ]; then
|
|
echo " user $username password $hash" >> "$tmpfile"
|
|
count=$((count + 1))
|
|
log_info " Added: $username"
|
|
else
|
|
log_warn " Skip: $username (no password hash)"
|
|
fi
|
|
done
|
|
|
|
# Move to final location
|
|
mv "$tmpfile" "$AUTH_USERLIST_FILE"
|
|
chmod 600 "$AUTH_USERLIST_FILE"
|
|
|
|
log_info "Synced $count users to $AUTH_USERLIST_FILE"
|
|
|
|
# Regenerate config to include userlist
|
|
generate_config
|
|
cmd_reload
|
|
|
|
log_info "HAProxy reloaded with updated userlist"
|
|
}
|
|
|
|
# Enable auth for a vhost (make it private)
|
|
cmd_auth_enable() {
|
|
local domain="$1"
|
|
[ -z "$domain" ] && { echo "Usage: haproxyctl auth enable <domain>"; return 1; }
|
|
|
|
# Find vhost section
|
|
local section=$(echo "$domain" | tr '.' '_' | tr '-' '_')
|
|
section="vhost_${section}"
|
|
|
|
local existing=$(uci -q get haproxy.${section}.domain)
|
|
if [ -z "$existing" ]; then
|
|
log_error "Vhost not found: $domain"
|
|
return 1
|
|
fi
|
|
|
|
uci set haproxy.${section}.auth_enabled='1'
|
|
uci commit haproxy
|
|
|
|
log_info "Auth enabled for $domain (private mode)"
|
|
|
|
# Regenerate config
|
|
generate_config
|
|
cmd_reload
|
|
|
|
log_info "To sync users: haproxyctl auth sync"
|
|
}
|
|
|
|
# Disable auth for a vhost (make it public)
|
|
cmd_auth_disable() {
|
|
local domain="$1"
|
|
[ -z "$domain" ] && { echo "Usage: haproxyctl auth disable <domain>"; return 1; }
|
|
|
|
local section=$(echo "$domain" | tr '.' '_' | tr '-' '_')
|
|
section="vhost_${section}"
|
|
|
|
local existing=$(uci -q get haproxy.${section}.domain)
|
|
if [ -z "$existing" ]; then
|
|
log_error "Vhost not found: $domain"
|
|
return 1
|
|
fi
|
|
|
|
uci set haproxy.${section}.auth_enabled='0'
|
|
uci commit haproxy
|
|
|
|
log_info "Auth disabled for $domain (public mode)"
|
|
|
|
generate_config
|
|
cmd_reload
|
|
}
|
|
|
|
# Show auth status for all vhosts
|
|
cmd_auth_status() {
|
|
echo "HAProxy Authentication Status"
|
|
echo "=============================="
|
|
echo ""
|
|
|
|
# Check if userlist exists
|
|
if [ -f "$AUTH_USERLIST_FILE" ]; then
|
|
local user_count=$(grep -c "^ user " "$AUTH_USERLIST_FILE" 2>/dev/null || echo 0)
|
|
echo "Userlist: $AUTH_USERLIST_FILE ($user_count users)"
|
|
echo ""
|
|
else
|
|
echo "Userlist: Not configured (run: haproxyctl auth sync)"
|
|
echo ""
|
|
fi
|
|
|
|
echo "Vhosts:"
|
|
echo "-------"
|
|
|
|
config_load haproxy
|
|
_print_auth_status() {
|
|
local section="$1"
|
|
local enabled domain auth_enabled
|
|
|
|
config_get enabled "$section" enabled "0"
|
|
[ "$enabled" = "1" ] || return
|
|
|
|
config_get domain "$section" domain
|
|
config_get auth_enabled "$section" auth_enabled "0"
|
|
|
|
[ -n "$domain" ] || return
|
|
|
|
if [ "$auth_enabled" = "1" ]; then
|
|
printf " %-35s [PRIVATE] (auth required)\n" "$domain"
|
|
else
|
|
printf " %-35s [PUBLIC]\n" "$domain"
|
|
fi
|
|
}
|
|
config_foreach _print_auth_status vhost
|
|
}
|
|
|
|
# Show authentication log
|
|
cmd_auth_log() {
|
|
local lines="${1:-50}"
|
|
echo "Authentication Log (last $lines entries)"
|
|
echo "========================================="
|
|
logread | grep -E "haproxy.*auth|HAProxy.*401|haproxy.*unauthorized" | tail -n "$lines"
|
|
}
|
|
|
|
# Generate userlist include for haproxy.cfg
|
|
_generate_userlist() {
|
|
if [ -f "$AUTH_USERLIST_FILE" ]; then
|
|
cat "$AUTH_USERLIST_FILE"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# ===========================================
|
|
# 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 "$@" ;;
|
|
verify) shift; cmd_cert_verify "$@" ;;
|
|
remove) shift; rm -f "$CERTS_PATH/$1.pem"; uci delete haproxy.cert_${1//[.-]/_} 2>/dev/null ;;
|
|
*) echo "Usage: haproxyctl cert {list|add|import|renew|verify|remove}" ;;
|
|
esac
|
|
;;
|
|
|
|
path)
|
|
shift
|
|
case "${1:-}" in
|
|
list) shift; cmd_path_list "$@" ;;
|
|
sync) shift; cmd_path_sync "$@" ;;
|
|
add) shift; cmd_path_add "$@" ;;
|
|
remove) shift; cmd_path_remove "$@" ;;
|
|
clear) shift; cmd_path_clear "$@" ;;
|
|
*) echo "Usage: haproxyctl path {list|sync|add|remove|clear}" ;;
|
|
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 "$@" ;;
|
|
|
|
auth)
|
|
shift
|
|
case "${1:-}" in
|
|
sync) shift; cmd_auth_sync "$@" ;;
|
|
enable) shift; cmd_auth_enable "$@" ;;
|
|
disable) shift; cmd_auth_disable "$@" ;;
|
|
status) shift; cmd_auth_status "$@" ;;
|
|
log) shift; cmd_auth_log "$@" ;;
|
|
*) echo "Usage: haproxyctl auth {sync|enable|disable|status|log}" ;;
|
|
esac
|
|
;;
|
|
|
|
shell) shift; lxc_exec /bin/sh ;;
|
|
exec) shift; lxc_exec "$@" ;;
|
|
|
|
*) usage ;;
|
|
esac
|