secubox-openwrt/package/secubox/secubox-app-vhost-manager/files/usr/sbin/secubox-vhost
CyberMind-FR e13b6e4c8c feat(vhost-manager): Add centralized VHost manager
- Create secubox-app-vhost-manager package for unified vhost orchestration
- Single CLI tool (secubox-vhost) manages HAProxy, DNS, Tor, Mesh, mitmproxy
- Unified UCI config (/etc/config/vhosts) as single source of truth
- Backend adapters for each component (haproxy.sh, dns.sh, tor.sh, mesh.sh, mitmproxy.sh)
- Centralized backend resolution function (backends.sh)
- Import tool for existing HAProxy vhosts
- Validation of backend reachability before creation

Also includes:
- FAQ-TROUBLESHOOTING.md with LXC cgroup v1/v2 fixes
- Fix mitmproxyctl cgroup v1 -> v2 syntax for container compatibility
- HAProxy backend resolution bugfixes

CLI commands:
  secubox-vhost add <domain> <service> <port> [--ssl] [--tor] [--mesh]
  secubox-vhost remove/list/status/enable/disable/set/sync/validate/import

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 10:16:19 +01:00

574 lines
17 KiB
Bash

#!/bin/sh
# SecuBox Centralized VHost Manager
# Orchestrates HAProxy, DNS, Tor, Mesh, and mitmproxy from single interface
VERSION="1.0.0"
CONFIG="vhosts"
# Load adapter libraries
LIB_DIR="/usr/lib/vhost-manager"
. "$LIB_DIR/backends.sh"
. "$LIB_DIR/haproxy.sh"
. "$LIB_DIR/dns.sh"
. "$LIB_DIR/tor.sh"
. "$LIB_DIR/mesh.sh"
. "$LIB_DIR/mitmproxy.sh"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${GREEN}[VHOST]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ============================================================================
# Add VHost
# ============================================================================
cmd_add() {
local domain="$1"
local service="$2"
local port="$3"
shift 3
if [ -z "$domain" ] || [ -z "$service" ] || [ -z "$port" ]; then
echo "Usage: secubox-vhost add <domain> <service> <port> [options]"
echo ""
echo "Options:"
echo " --ssl Enable SSL (default: yes)"
echo " --no-ssl Disable SSL"
echo " --tor Enable Tor hidden service"
echo " --mesh Enable mesh P2P publishing"
echo " --mitm Enable mitmproxy WAF (default: yes)"
echo " --no-mitm Disable mitmproxy WAF"
return 1
fi
# Validate port
if ! echo "$port" | grep -qE '^[0-9]+$' || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
error "Invalid port: $port (must be 1-65535)"
return 1
fi
# Parse options
local ssl=1
local tor=0
local mesh=0
local mitm=1
local host="127.0.0.1"
while [ $# -gt 0 ]; do
case "$1" in
--ssl) ssl=1 ;;
--no-ssl) ssl=0 ;;
--tor) tor=1 ;;
--mesh) mesh=1 ;;
--mitm) mitm=1 ;;
--no-mitm) mitm=0 ;;
--host=*) host="${1#*=}" ;;
*) warn "Unknown option: $1" ;;
esac
shift
done
local section=$(sanitize_section_name "$domain")
# Check for duplicate
if uci -q get $CONFIG.$section >/dev/null 2>&1; then
error "VHost already exists: $domain"
return 1
fi
log "Creating vhost: $domain -> $host:$port"
# Store in UCI
uci set $CONFIG.$section=vhost
uci set $CONFIG.$section.domain="$domain"
uci set $CONFIG.$section.service="$service"
uci set $CONFIG.$section.backend_host="$host"
uci set $CONFIG.$section.backend_port="$port"
uci set $CONFIG.$section.enabled='1'
uci set $CONFIG.$section.haproxy='1'
uci set $CONFIG.$section.ssl="$ssl"
uci set $CONFIG.$section.ssl_redirect="$ssl"
uci set $CONFIG.$section.acme="$ssl"
uci set $CONFIG.$section.tor="$tor"
uci set $CONFIG.$section.mesh="$mesh"
uci set $CONFIG.$section.mitmproxy="$mitm"
uci set $CONFIG.$section.created="$(date -Iseconds)"
uci commit $CONFIG
# Apply to backends
log " [1/5] Creating HAProxy vhost..."
haproxy_add_vhost "$domain" "$service" "$host" "$port" "$ssl" "$ssl"
log " [2/5] Adding DNS record..."
dns_add_record "$domain" 2>/dev/null || warn "DNS: Not configured or failed"
if [ "$tor" = "1" ]; then
log " [3/5] Creating Tor hidden service..."
if tor_is_available; then
tor_add_service "$service" "$port"
local onion=$(tor_wait_for_onion "$service" 5)
if [ -n "$onion" ]; then
uci set $CONFIG.$section.onion_address="$onion"
uci commit $CONFIG
log " Onion: $onion"
fi
else
warn "Tor not available"
fi
else
log " [3/5] Tor: skipped"
fi
if [ "$mesh" = "1" ]; then
log " [4/5] Publishing to mesh..."
if mesh_is_available; then
mesh_publish "$service" "$port" "$domain"
else
warn "Mesh not available"
fi
else
log " [4/5] Mesh: skipped"
fi
if [ "$mitm" = "1" ]; then
log " [5/5] Adding mitmproxy route..."
if mitmproxy_is_available; then
mitmproxy_add_route "$domain" "$host" "$port"
else
warn "Mitmproxy not available"
fi
else
log " [5/5] Mitmproxy: skipped"
fi
# Reload HAProxy
log "Reloading HAProxy..."
haproxy_reload
echo ""
log "VHost created: https://$domain"
[ "$tor" = "1" ] && [ -n "$onion" ] && log "Tor: http://$onion"
}
# ============================================================================
# Remove VHost
# ============================================================================
cmd_remove() {
local domain="$1"
if [ -z "$domain" ]; then
echo "Usage: secubox-vhost remove <domain>"
return 1
fi
local section=$(sanitize_section_name "$domain")
if ! uci -q get $CONFIG.$section >/dev/null 2>&1; then
error "VHost not found: $domain"
return 1
fi
local service=$(uci -q get $CONFIG.$section.service)
local tor=$(uci -q get $CONFIG.$section.tor)
local mesh=$(uci -q get $CONFIG.$section.mesh)
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
log "Removing vhost: $domain"
# Remove from backends
log " [1/5] Removing HAProxy vhost..."
haproxy_remove_vhost "$domain"
log " [2/5] Removing DNS record..."
dns_remove_record "$domain" 2>/dev/null || true
if [ "$tor" = "1" ]; then
log " [3/5] Removing Tor hidden service..."
tor_remove_service "$service" 2>/dev/null || true
fi
if [ "$mesh" = "1" ]; then
log " [4/5] Unpublishing from mesh..."
mesh_unpublish "$service" 2>/dev/null || true
fi
if [ "$mitm" = "1" ]; then
log " [5/5] Removing mitmproxy route..."
mitmproxy_remove_route "$domain" 2>/dev/null || true
fi
# Remove from UCI
uci delete $CONFIG.$section
uci commit $CONFIG
# Reload HAProxy
haproxy_reload
log "VHost removed: $domain"
}
# ============================================================================
# List VHosts
# ============================================================================
cmd_list() {
local json=0
[ "$1" = "--json" ] && json=1
if [ "$json" = "1" ]; then
printf '{"vhosts":['
local first=1
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
local domain=$(uci -q get $CONFIG.$section.domain)
[ -z "$domain" ] && continue
[ $first -eq 0 ] && printf ','
first=0
local service=$(uci -q get $CONFIG.$section.service)
local port=$(uci -q get $CONFIG.$section.backend_port)
local enabled=$(uci -q get $CONFIG.$section.enabled)
local ssl=$(uci -q get $CONFIG.$section.ssl)
local tor=$(uci -q get $CONFIG.$section.tor)
local mesh=$(uci -q get $CONFIG.$section.mesh)
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
local onion=$(uci -q get $CONFIG.$section.onion_address)
printf '{"domain":"%s","service":"%s","port":%s,"enabled":%s,"ssl":%s,"tor":%s,"mesh":%s,"mitmproxy":%s,"onion":"%s"}' \
"$domain" "${service:-unknown}" "${port:-0}" \
"$([ "$enabled" = "1" ] && echo true || echo false)" \
"$([ "$ssl" = "1" ] && echo true || echo false)" \
"$([ "$tor" = "1" ] && echo true || echo false)" \
"$([ "$mesh" = "1" ] && echo true || echo false)" \
"$([ "$mitm" = "1" ] && echo true || echo false)" \
"${onion:-}"
done
printf ']}\n'
else
echo ""
echo "==========================================="
echo " SecuBox VHosts"
echo "==========================================="
echo ""
printf "%-30s %-12s %-6s %-4s %-4s %-4s %-4s\n" "DOMAIN" "SERVICE" "PORT" "SSL" "TOR" "MESH" "WAF"
printf "%-30s %-12s %-6s %-4s %-4s %-4s %-4s\n" "------" "-------" "----" "---" "---" "----" "---"
local count=0
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
local domain=$(uci -q get $CONFIG.$section.domain)
[ -z "$domain" ] && continue
local service=$(uci -q get $CONFIG.$section.service)
local port=$(uci -q get $CONFIG.$section.backend_port)
local ssl=$(uci -q get $CONFIG.$section.ssl)
local tor=$(uci -q get $CONFIG.$section.tor)
local mesh=$(uci -q get $CONFIG.$section.mesh)
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
printf "%-30s %-12s %-6s %-4s %-4s %-4s %-4s\n" \
"$domain" "${service:-?}" "${port:-?}" \
"$([ "$ssl" = "1" ] && echo "yes" || echo "no")" \
"$([ "$tor" = "1" ] && echo "yes" || echo "no")" \
"$([ "$mesh" = "1" ] && echo "yes" || echo "no")" \
"$([ "$mitm" = "1" ] && echo "yes" || echo "no")"
count=$((count + 1))
done
if [ "$count" -eq 0 ]; then
echo " No vhosts configured"
fi
echo ""
echo "Total: $count vhost(s)"
echo ""
fi
}
# ============================================================================
# Status
# ============================================================================
cmd_status() {
local domain="$1"
if [ -z "$domain" ]; then
echo "Usage: secubox-vhost status <domain>"
return 1
fi
local section=$(sanitize_section_name "$domain")
if ! uci -q get $CONFIG.$section >/dev/null 2>&1; then
error "VHost not found: $domain"
return 1
fi
echo ""
echo "VHost: $domain"
echo "========================================"
echo ""
echo "Service: $(uci -q get $CONFIG.$section.service)"
echo "Backend: $(uci -q get $CONFIG.$section.backend_host):$(uci -q get $CONFIG.$section.backend_port)"
echo "Enabled: $(uci -q get $CONFIG.$section.enabled)"
echo ""
echo "Channels:"
echo " HAProxy: yes ($(haproxy_get_status "$domain"))"
echo " SSL/ACME: $(uci -q get $CONFIG.$section.ssl)"
echo " Tor: $(uci -q get $CONFIG.$section.tor)"
[ "$(uci -q get $CONFIG.$section.tor)" = "1" ] && echo " Onion: $(uci -q get $CONFIG.$section.onion_address)"
echo " Mesh: $(uci -q get $CONFIG.$section.mesh)"
echo " Mitmproxy: $(uci -q get $CONFIG.$section.mitmproxy)"
echo ""
echo "Created: $(uci -q get $CONFIG.$section.created)"
echo ""
}
# ============================================================================
# Sync
# ============================================================================
cmd_sync() {
log "Syncing vhosts to all backends..."
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
local domain=$(uci -q get $CONFIG.$section.domain)
[ -z "$domain" ] && continue
local service=$(uci -q get $CONFIG.$section.service)
local host=$(uci -q get $CONFIG.$section.backend_host)
local port=$(uci -q get $CONFIG.$section.backend_port)
local ssl=$(uci -q get $CONFIG.$section.ssl)
local tor=$(uci -q get $CONFIG.$section.tor)
local mesh=$(uci -q get $CONFIG.$section.mesh)
local mitm=$(uci -q get $CONFIG.$section.mitmproxy)
log " Syncing: $domain"
# Ensure HAProxy
haproxy_add_vhost "$domain" "$service" "$host" "$port" "$ssl" "$ssl" 2>/dev/null
# Ensure mitmproxy route
[ "$mitm" = "1" ] && mitmproxy_add_route "$domain" "$host" "$port" 2>/dev/null
done
haproxy_reload
mitmproxy_sync_routes 2>/dev/null || true
log "Sync complete"
}
# ============================================================================
# Validate
# ============================================================================
cmd_validate() {
log "Validating vhosts..."
local errors=0
for section in $(uci show $CONFIG 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
local domain=$(uci -q get $CONFIG.$section.domain)
[ -z "$domain" ] && continue
local host=$(uci -q get $CONFIG.$section.backend_host)
local port=$(uci -q get $CONFIG.$section.backend_port)
if validate_backend "$host:$port"; then
log " [OK] $domain -> $host:$port"
else
error " [FAIL] $domain -> $host:$port (unreachable)"
errors=$((errors + 1))
fi
done
if [ "$errors" -gt 0 ]; then
error "$errors vhost(s) have unreachable backends"
return 1
fi
log "All vhosts valid"
}
# ============================================================================
# Import
# ============================================================================
cmd_import() {
log "Importing vhosts from HAProxy UCI..."
local imported=0
for section in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do
local domain=$(uci -q get haproxy.$section.domain)
[ -z "$domain" ] && continue
local vhost_section=$(sanitize_section_name "$domain")
# Skip if already imported
uci -q get $CONFIG.$vhost_section >/dev/null 2>&1 && continue
local backend=$(uci -q get haproxy.$section.backend)
local addr=$(get_backend_address "" "$backend")
local host=$(echo "$addr" | cut -d: -f1)
local port=$(echo "$addr" | cut -d: -f2)
[ -z "$port" ] && port="80"
[ -z "$host" ] && host="127.0.0.1"
local ssl=$(uci -q get haproxy.$section.ssl)
local enabled=$(uci -q get haproxy.$section.enabled)
log " Importing: $domain ($backend -> $host:$port)"
uci set $CONFIG.$vhost_section=vhost
uci set $CONFIG.$vhost_section.domain="$domain"
uci set $CONFIG.$vhost_section.service="${backend#backend_}"
uci set $CONFIG.$vhost_section.backend_host="$host"
uci set $CONFIG.$vhost_section.backend_port="$port"
uci set $CONFIG.$vhost_section.enabled="${enabled:-1}"
uci set $CONFIG.$vhost_section.haproxy='1'
uci set $CONFIG.$vhost_section.ssl="${ssl:-1}"
uci set $CONFIG.$vhost_section.ssl_redirect="${ssl:-1}"
uci set $CONFIG.$vhost_section.acme="${ssl:-1}"
uci set $CONFIG.$vhost_section.tor='0'
uci set $CONFIG.$vhost_section.mesh='0'
uci set $CONFIG.$vhost_section.mitmproxy='1'
uci set $CONFIG.$vhost_section.imported='1'
imported=$((imported + 1))
done
uci commit $CONFIG
log "Imported $imported vhost(s)"
}
# ============================================================================
# Enable/Disable
# ============================================================================
cmd_enable() {
local domain="$1"
[ -z "$domain" ] && { echo "Usage: secubox-vhost enable <domain>"; return 1; }
local section=$(sanitize_section_name "$domain")
uci set $CONFIG.$section.enabled='1'
uci commit $CONFIG
haproxy_set_enabled "$domain" '1'
haproxy_reload
log "Enabled: $domain"
}
cmd_disable() {
local domain="$1"
[ -z "$domain" ] && { echo "Usage: secubox-vhost disable <domain>"; return 1; }
local section=$(sanitize_section_name "$domain")
uci set $CONFIG.$section.enabled='0'
uci commit $CONFIG
haproxy_set_enabled "$domain" '0'
haproxy_reload
log "Disabled: $domain"
}
# ============================================================================
# Set Channel
# ============================================================================
cmd_set() {
local domain="$1"
shift
[ -z "$domain" ] && { echo "Usage: secubox-vhost set <domain> --<channel>=<0|1>"; return 1; }
local section=$(sanitize_section_name "$domain")
if ! uci -q get $CONFIG.$section >/dev/null 2>&1; then
error "VHost not found: $domain"
return 1
fi
while [ $# -gt 0 ]; do
case "$1" in
--ssl=*) uci set $CONFIG.$section.ssl="${1#*=}" ;;
--tor=*) uci set $CONFIG.$section.tor="${1#*=}" ;;
--mesh=*) uci set $CONFIG.$section.mesh="${1#*=}" ;;
--mitmproxy=*|--mitm=*) uci set $CONFIG.$section.mitmproxy="${1#*=}" ;;
*) warn "Unknown option: $1" ;;
esac
shift
done
uci commit $CONFIG
cmd_sync
log "Updated: $domain"
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << EOF
SecuBox Centralized VHost Manager v${VERSION}
Usage: secubox-vhost <command> [options]
Commands:
add <domain> <service> <port> [opts] Create new vhost
remove <domain> Remove vhost
list [--json] List all vhosts
status <domain> Show vhost details
enable <domain> Enable vhost
disable <domain> Disable vhost
set <domain> --<channel>=<0|1> Toggle channel
sync Sync UCI to all backends
validate Validate all backends reachable
import Import from HAProxy UCI
Add Options:
--ssl / --no-ssl Enable/disable SSL (default: yes)
--tor Enable Tor hidden service
--mesh Enable mesh P2P publishing
--mitm / --no-mitm Enable/disable mitmproxy WAF (default: yes)
--host=<ip> Backend host (default: 127.0.0.1)
Examples:
secubox-vhost add git.example.com gitea 3000 --ssl --tor
secubox-vhost add api.example.com myapi 8080 --ssl --mesh --mitm
secubox-vhost remove git.example.com
secubox-vhost set api.example.com --tor=1
secubox-vhost list --json
secubox-vhost import
EOF
}
# ============================================================================
# Main
# ============================================================================
case "${1:-}" in
add) shift; cmd_add "$@" ;;
remove|rm) shift; cmd_remove "$@" ;;
list|ls) shift; cmd_list "$@" ;;
status) shift; cmd_status "$@" ;;
enable) shift; cmd_enable "$@" ;;
disable) shift; cmd_disable "$@" ;;
set) shift; cmd_set "$@" ;;
sync) shift; cmd_sync "$@" ;;
validate) shift; cmd_validate "$@" ;;
import) shift; cmd_import "$@" ;;
help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac
exit 0