secubox-openwrt/package/secubox/secubox-app-exposure/files/usr/sbin/secubox-exposure
CyberMind-FR c9075bc190 feat(haproxy): Add exposed services integration and fix cert key naming
- Fix HAProxy certificate key naming (.key -> .crt.key) for directory loading
- Add auto-fix in container startup script for existing certificates
- Add list_exposed_services RPC method to fetch services from secubox-exposure
- Add dynamic port scanning for running services discovery
- Add "Quick Select" dropdown in Add Server modal for service auto-fill
- Bump luci-app-haproxy to 1.0.0-r8
- Bump secubox-app-haproxy to 1.0.0-r15

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 08:34:57 +01:00

734 lines
23 KiB
Bash
Executable File

#!/bin/sh
#
# SecuBox Service Exposure Manager
# Unified tool for port management, Tor hidden services, and HAProxy SSL
#
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CONFIG_NAME="secubox-exposure"
HAPROXY_CONFIG=""
HAPROXY_CERTS=""
TOR_HIDDEN_DIR=""
TOR_CONFIG=""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_err() { echo -e "${RED}[ERROR]${NC} $1"; }
load_config() {
config_load "$CONFIG_NAME"
config_get HAPROXY_CONFIG main haproxy_config "/srv/lxc/haproxy/rootfs/etc/haproxy/haproxy.cfg"
config_get HAPROXY_CERTS main haproxy_certs "/srv/lxc/haproxy/rootfs/etc/haproxy/certs"
config_get TOR_HIDDEN_DIR main tor_hidden_dir "/var/lib/tor/hidden_services"
config_get TOR_CONFIG main tor_config "/etc/tor/torrc"
config_get APP_PORT_START ranges app_start "8100"
config_get APP_PORT_END ranges app_end "8199"
}
# ============================================================================
# PORT SCANNING & CONFLICT DETECTION
# ============================================================================
get_listening_ports() {
# Returns: port address process
netstat -tlnp 2>/dev/null | grep LISTEN | awk '{
split($4, a, ":")
port = a[length(a)]
if (!seen[port]++) {
split($7, p, "/")
proc = p[2]
if (proc == "") proc = "unknown"
print port, $4, proc
}
}' | sort -n
}
cmd_scan() {
log_info "Scanning listening services..."
echo ""
printf "%-6s %-20s %-15s %-10s\n" "PORT" "ADDRESS" "PROCESS" "STATUS"
printf "%-6s %-20s %-15s %-10s\n" "------" "--------------------" "---------------" "----------"
get_listening_ports | while read port addr proc; do
# Determine if external
case "$addr" in
*0.0.0.0*|*::*) status="${GREEN}external${NC}" ;;
*127.0.0.1*|*::1*) status="${YELLOW}local${NC}" ;;
*) status="${CYAN}bound${NC}" ;;
esac
printf "%-6s %-20s %-15s " "$port" "$addr" "$proc"
echo -e "$status"
done
echo ""
}
cmd_conflicts() {
log_info "Checking for port conflicts..."
echo ""
local conflicts=0
local TMP_PORTS="/tmp/ports_$$"
# Get all configured ports from UCI
> "$TMP_PORTS"
# Check known services
check_known_service() {
local section="$1"
local default_port config_path
config_get default_port "$section" default_port
config_get config_path "$section" config_path
if [ -n "$config_path" ]; then
# Extract UCI config and option
local uci_config=$(echo "$config_path" | cut -d'.' -f1)
local uci_option=$(echo "$config_path" | cut -d'.' -f2-)
local actual_port=$(uci -q get "$config_path" 2>/dev/null)
[ -z "$actual_port" ] && actual_port="$default_port"
echo "$actual_port $section" >> "$TMP_PORTS"
fi
}
config_foreach check_known_service known
# Find duplicates
sort "$TMP_PORTS" | uniq -d -w5 | while read port svc; do
log_warn "Port $port is configured for multiple services!"
grep "^$port " "$TMP_PORTS" | while read p s; do
echo " - $s"
done
conflicts=$((conflicts + 1))
done
# Check against actually listening ports
get_listening_ports | while read port addr proc; do
if grep -q "^$port " "$TMP_PORTS"; then
local configured_svc=$(grep "^$port " "$TMP_PORTS" | head -1 | cut -d' ' -f2)
# Check if process matches expected
case "$configured_svc" in
gitea) [ "$proc" != "gitea" ] && log_warn "Port $port: expected gitea, found $proc" ;;
streamlit) echo "$proc" | grep -qv "python\|streamlit" && log_warn "Port $port: expected streamlit, found $proc" ;;
esac
fi
done
rm -f "$TMP_PORTS"
if [ "$conflicts" -eq 0 ]; then
log_ok "No port conflicts detected"
fi
}
find_free_port() {
local start="$1"
local end="$2"
local port="$start"
while [ "$port" -le "$end" ]; do
if ! netstat -tlnp 2>/dev/null | grep -q ":$port "; then
echo "$port"
return 0
fi
port=$((port + 1))
done
return 1
}
cmd_fix_port() {
local service="$1"
local new_port="$2"
if [ -z "$service" ]; then
log_err "Usage: secubox-exposure fix-port <service> [new_port]"
return 1
fi
load_config
# Get service config
local config_path default_port
config_get config_path "$service" config_path
config_get default_port "$service" default_port
if [ -z "$config_path" ]; then
log_err "Unknown service: $service"
return 1
fi
# Find free port if not specified
if [ -z "$new_port" ]; then
new_port=$(find_free_port "$APP_PORT_START" "$APP_PORT_END")
if [ -z "$new_port" ]; then
log_err "No free ports available in range $APP_PORT_START-$APP_PORT_END"
return 1
fi
fi
# Check if new port is free
if netstat -tlnp 2>/dev/null | grep -q ":$new_port "; then
log_err "Port $new_port is already in use"
return 1
fi
log_info "Changing $service port to $new_port"
# Update UCI
if uci set "$config_path=$new_port" && uci commit; then
log_ok "UCI config updated"
# Restart service if it has an init script
if [ -x "/etc/init.d/$service" ]; then
log_info "Restarting $service..."
/etc/init.d/"$service" restart
fi
log_ok "$service now listening on port $new_port"
else
log_err "Failed to update UCI config"
return 1
fi
}
# ============================================================================
# TOR HIDDEN SERVICES
# ============================================================================
cmd_tor_add() {
local service="$1"
local local_port="$2"
local onion_port="${3:-80}"
if [ -z "$service" ]; then
log_err "Usage: secubox-exposure tor add <service> [local_port] [onion_port]"
return 1
fi
load_config
# Get local port from config if not specified
if [ -z "$local_port" ]; then
config_get local_port "$service" default_port
if [ -z "$local_port" ]; then
log_err "Cannot determine local port for $service"
return 1
fi
fi
local hidden_dir="$TOR_HIDDEN_DIR/$service"
# Create hidden service directory
mkdir -p "$hidden_dir"
chmod 700 "$hidden_dir"
chown tor:tor "$hidden_dir" 2>/dev/null || chown debian-tor:debian-tor "$hidden_dir" 2>/dev/null
# Check if already configured in torrc
if grep -q "HiddenServiceDir $hidden_dir" "$TOR_CONFIG" 2>/dev/null; then
log_warn "Hidden service for $service already exists"
local onion=$(cat "$hidden_dir/hostname" 2>/dev/null)
[ -n "$onion" ] && log_info "Onion address: $onion"
return 0
fi
# Add to torrc
log_info "Adding hidden service for $service (127.0.0.1:$local_port -> :$onion_port)"
cat >> "$TOR_CONFIG" << EOF
# Hidden service for $service (added by secubox-exposure)
HiddenServiceDir $hidden_dir
HiddenServicePort $onion_port 127.0.0.1:$local_port
EOF
# Restart Tor
log_info "Restarting Tor..."
/etc/init.d/tor restart 2>/dev/null || systemctl restart tor 2>/dev/null
# Wait for onion address
log_info "Waiting for onion address generation..."
local tries=0
while [ ! -f "$hidden_dir/hostname" ] && [ "$tries" -lt 30 ]; do
sleep 1
tries=$((tries + 1))
done
if [ -f "$hidden_dir/hostname" ]; then
local onion=$(cat "$hidden_dir/hostname")
log_ok "Hidden service created!"
echo ""
echo -e " ${CYAN}Service:${NC} $service"
echo -e " ${CYAN}Onion:${NC} $onion"
echo -e " ${CYAN}Port:${NC} $onion_port -> 127.0.0.1:$local_port"
echo ""
# Save to exposure UCI
uci set "${CONFIG_NAME}.${service}=service"
uci set "${CONFIG_NAME}.${service}.port=$local_port"
uci set "${CONFIG_NAME}.${service}.tor=1"
uci set "${CONFIG_NAME}.${service}.tor_onion=$onion"
uci set "${CONFIG_NAME}.${service}.tor_port=$onion_port"
uci commit "$CONFIG_NAME"
# Sync to Tor Shield UCI
local hs_name="hs_${service}"
uci set "tor-shield.${hs_name}=hidden_service"
uci set "tor-shield.${hs_name}.name=${service}"
uci set "tor-shield.${hs_name}.enabled=1"
uci set "tor-shield.${hs_name}.local_port=${local_port}"
uci set "tor-shield.${hs_name}.onion_port=${onion_port}"
uci set "tor-shield.${hs_name}.onion_address=${onion}"
uci commit tor-shield
log_ok "Synced to Tor Shield"
else
log_err "Failed to generate onion address"
return 1
fi
}
cmd_tor_list() {
load_config
log_info "Tor Hidden Services:"
echo ""
printf "%-15s %-62s %-10s\n" "SERVICE" "ONION ADDRESS" "PORT"
printf "%-15s %-62s %-10s\n" "---------------" "--------------------------------------------------------------" "----------"
# List from filesystem
if [ -d "$TOR_HIDDEN_DIR" ]; then
for dir in "$TOR_HIDDEN_DIR"/*/; do
[ -d "$dir" ] || continue
local svc=$(basename "$dir")
local onion=""
[ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname")
# Get port from torrc
local port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $2}')
if [ -n "$onion" ]; then
printf "%-15s %-62s %-10s\n" "$svc" "$onion" "${port:-80}"
fi
done
fi
echo ""
}
cmd_tor_remove() {
local service="$1"
if [ -z "$service" ]; then
log_err "Usage: secubox-exposure tor remove <service>"
return 1
fi
load_config
local hidden_dir="$TOR_HIDDEN_DIR/$service"
if [ ! -d "$hidden_dir" ]; then
log_err "No hidden service found for $service"
return 1
fi
log_info "Removing hidden service for $service"
# Remove from torrc (remove the block)
sed -i "/# Hidden service for $service/,/HiddenServicePort/d" "$TOR_CONFIG"
# Remove directory
rm -rf "$hidden_dir"
# Update exposure UCI
uci delete "${CONFIG_NAME}.${service}.tor" 2>/dev/null
uci delete "${CONFIG_NAME}.${service}.tor_onion" 2>/dev/null
uci delete "${CONFIG_NAME}.${service}.tor_port" 2>/dev/null
uci commit "$CONFIG_NAME"
# Remove from Tor Shield UCI
local hs_name="hs_${service}"
if uci -q get "tor-shield.${hs_name}" >/dev/null 2>&1; then
uci delete "tor-shield.${hs_name}"
uci commit tor-shield
log_ok "Removed from Tor Shield"
fi
# Restart Tor
/etc/init.d/tor restart 2>/dev/null || systemctl restart tor 2>/dev/null
log_ok "Hidden service removed"
}
cmd_tor_sync() {
load_config
log_info "Syncing hidden services to Tor Shield..."
local synced=0
# List from filesystem and sync to Tor Shield
if [ -d "$TOR_HIDDEN_DIR" ]; then
for dir in "$TOR_HIDDEN_DIR"/*/; do
[ -d "$dir" ] || continue
local svc=$(basename "$dir")
local onion=""
[ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname")
# Get port from torrc
local port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $2}')
local local_port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{split($3,a,":"); print a[2]}')
if [ -n "$onion" ]; then
local hs_name="hs_${svc}"
if ! uci -q get "tor-shield.${hs_name}" >/dev/null 2>&1; then
log_info "Adding $svc to Tor Shield"
uci set "tor-shield.${hs_name}=hidden_service"
uci set "tor-shield.${hs_name}.name=${svc}"
uci set "tor-shield.${hs_name}.enabled=1"
uci set "tor-shield.${hs_name}.local_port=${local_port:-80}"
uci set "tor-shield.${hs_name}.onion_port=${port:-80}"
uci set "tor-shield.${hs_name}.onion_address=${onion}"
synced=$((synced + 1))
fi
fi
done
fi
if [ "$synced" -gt 0 ]; then
uci commit tor-shield
log_ok "Synced $synced hidden service(s) to Tor Shield"
else
log_info "All hidden services already synced"
fi
}
# ============================================================================
# HAPROXY SSL BACKENDS (UCI-based integration with haproxyctl)
# ============================================================================
# Sanitize name for UCI section (replace dots/hyphens with underscores)
sanitize_uci_name() {
echo "$1" | sed 's/[.-]/_/g'
}
cmd_ssl_add() {
local service="$1"
local domain="$2"
local local_port="$3"
if [ -z "$service" ] || [ -z "$domain" ]; then
log_err "Usage: secubox-exposure ssl add <service> <domain> [local_port]"
return 1
fi
load_config
# Get local port from config if not specified
if [ -z "$local_port" ]; then
config_get local_port "$service" default_port
# Try to get from service UCI
local config_path
config_get config_path "$service" config_path
if [ -n "$config_path" ]; then
local configured_port=$(uci -q get "$config_path")
[ -n "$configured_port" ] && local_port="$configured_port"
fi
if [ -z "$local_port" ]; then
log_err "Cannot determine local port for $service. Specify it manually."
return 1
fi
fi
# Check if haproxyctl exists
if [ ! -x "/usr/sbin/haproxyctl" ]; then
log_err "haproxyctl not found. Is secubox-app-haproxy installed?"
return 1
fi
# Sanitize names for UCI
local backend_name="$service"
local vhost_name=$(sanitize_uci_name "$domain")
# Check if backend already exists in UCI
if uci -q get "haproxy.${backend_name}" >/dev/null 2>&1; then
log_warn "Backend '$backend_name' already exists in HAProxy UCI config"
else
# Create backend in HAProxy UCI config
log_info "Adding backend '$backend_name' (127.0.0.1:$local_port)"
uci set "haproxy.${backend_name}=backend"
uci set "haproxy.${backend_name}.name=${backend_name}"
uci set "haproxy.${backend_name}.mode=http"
uci set "haproxy.${backend_name}.balance=roundrobin"
uci set "haproxy.${backend_name}.enabled=1"
uci add_list "haproxy.${backend_name}.server=${service} 127.0.0.1:${local_port} check"
fi
# Check if vhost already exists
if uci -q get "haproxy.${vhost_name}" >/dev/null 2>&1; then
log_warn "Vhost for '$domain' already exists"
else
# Create vhost in HAProxy UCI config
log_info "Adding vhost '$domain' -> backend '$backend_name'"
uci set "haproxy.${vhost_name}=vhost"
uci set "haproxy.${vhost_name}.domain=${domain}"
uci set "haproxy.${vhost_name}.backend=${backend_name}"
uci set "haproxy.${vhost_name}.ssl=1"
uci set "haproxy.${vhost_name}.ssl_redirect=1"
uci set "haproxy.${vhost_name}.enabled=1"
fi
# Commit HAProxy UCI changes
uci commit haproxy
# Also save to exposure UCI for tracking
uci set "${CONFIG_NAME}.${service}=service"
uci set "${CONFIG_NAME}.${service}.port=$local_port"
uci set "${CONFIG_NAME}.${service}.ssl=1"
uci set "${CONFIG_NAME}.${service}.ssl_domain=$domain"
uci commit "$CONFIG_NAME"
log_ok "HAProxy UCI config updated"
log_info "Domain: $domain -> 127.0.0.1:$local_port"
# Regenerate and reload HAProxy
log_info "Regenerating HAProxy config..."
/usr/sbin/haproxyctl generate
log_info "Reloading HAProxy..."
/usr/sbin/haproxyctl reload
log_ok "SSL backend configured"
log_warn "Note: Ensure SSL certificate exists for $domain"
}
cmd_ssl_list() {
load_config
log_info "HAProxy SSL Backends:"
echo ""
printf "%-15s %-30s %-20s\n" "SERVICE" "DOMAIN" "BACKEND"
printf "%-15s %-30s %-20s\n" "---------------" "------------------------------" "--------------------"
# Read from HAProxy UCI config (vhosts with their backends)
local found=0
for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
local domain=$(uci -q get "haproxy.${vhost}.domain")
local backend=$(uci -q get "haproxy.${vhost}.backend")
local enabled=$(uci -q get "haproxy.${vhost}.enabled")
[ "$enabled" != "1" ] && continue
[ -z "$domain" ] && continue
# Get server from backend
local server=""
if [ -n "$backend" ]; then
server=$(uci -q get "haproxy.${backend}.server" | head -1 | awk '{print $2}')
fi
printf "%-15s %-30s %-20s\n" "${backend:-N/A}" "$domain" "${server:-N/A}"
found=1
done
[ "$found" = "0" ] && echo " No SSL backends configured"
echo ""
}
cmd_ssl_remove() {
local service="$1"
if [ -z "$service" ]; then
log_err "Usage: secubox-exposure ssl remove <service>"
return 1
fi
load_config
# Check if haproxyctl exists
if [ ! -x "/usr/sbin/haproxyctl" ]; then
log_err "haproxyctl not found"
return 1
fi
local backend_name="$service"
local removed=0
# Find and remove vhosts pointing to this backend
for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
local vhost_backend=$(uci -q get "haproxy.${vhost}.backend")
if [ "$vhost_backend" = "$backend_name" ]; then
log_info "Removing vhost '$vhost'"
uci delete "haproxy.${vhost}"
removed=1
fi
done
# Remove backend if it exists
if uci -q get "haproxy.${backend_name}" >/dev/null 2>&1; then
log_info "Removing backend '$backend_name'"
uci delete "haproxy.${backend_name}"
removed=1
fi
if [ "$removed" = "0" ]; then
log_err "No backend or vhost found for '$service'"
return 1
fi
# Commit HAProxy UCI changes
uci commit haproxy
# Update exposure UCI
uci delete "${CONFIG_NAME}.${service}.ssl" 2>/dev/null
uci delete "${CONFIG_NAME}.${service}.ssl_domain" 2>/dev/null
uci commit "$CONFIG_NAME"
# Regenerate and reload HAProxy
log_info "Regenerating HAProxy config..."
/usr/sbin/haproxyctl generate
log_info "Reloading HAProxy..."
/usr/sbin/haproxyctl reload
log_ok "SSL backend removed"
}
# ============================================================================
# STATUS & HELP
# ============================================================================
cmd_status() {
load_config
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} SecuBox Service Exposure Status${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
# Count services
local total_services=$(get_listening_ports | wc -l)
local external_services=$(get_listening_ports | grep -E "0\.0\.0\.0|::" | wc -l)
echo -e "${BLUE}Services:${NC}"
echo " Total listening: $total_services"
echo " External (0.0.0.0): $external_services"
echo ""
# Tor status
local tor_services=0
[ -d "$TOR_HIDDEN_DIR" ] && tor_services=$(ls -1 "$TOR_HIDDEN_DIR" 2>/dev/null | wc -l)
echo -e "${BLUE}Tor Hidden Services:${NC} $tor_services"
if [ "$tor_services" -gt 0 ]; then
for dir in "$TOR_HIDDEN_DIR"/*/; do
[ -d "$dir" ] || continue
local svc=$(basename "$dir")
local onion=$(cat "$dir/hostname" 2>/dev/null)
[ -n "$onion" ] && echo " - $svc: ${onion:0:16}..."
done
fi
echo ""
# HAProxy backends (from UCI)
local ssl_backends=0
echo -e "${BLUE}HAProxy SSL Backends:${NC}"
for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
local domain=$(uci -q get "haproxy.${vhost}.domain")
local backend=$(uci -q get "haproxy.${vhost}.backend")
local enabled=$(uci -q get "haproxy.${vhost}.enabled")
[ "$enabled" != "1" ] && continue
[ -z "$domain" ] && continue
echo " - ${backend}: ${domain}"
ssl_backends=$((ssl_backends + 1))
done
[ "$ssl_backends" = "0" ] && echo " (none configured)"
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
}
cmd_help() {
cat << EOF
SecuBox Service Exposure Manager
Usage: secubox-exposure <command> [options]
COMMANDS:
scan Scan all listening services
conflicts Detect port conflicts
fix-port <svc> [port] Change service port (auto-assigns if no port given)
status Show exposure status summary
tor add <svc> [port] Create Tor hidden service
tor list List hidden services
tor remove <svc> Remove hidden service
tor sync Sync hidden services to Tor Shield
ssl add <svc> <domain> Add HAProxy SSL backend
ssl list List SSL backends
ssl remove <svc> Remove SSL backend
EXAMPLES:
secubox-exposure scan
secubox-exposure conflicts
secubox-exposure fix-port domoticz 8180
secubox-exposure tor add gitea
secubox-exposure tor add streamlit 8501 80
secubox-exposure tor list
secubox-exposure ssl add gitea git.example.com
secubox-exposure ssl add streamlit app.example.com 8501
secubox-exposure ssl list
EOF
}
# ============================================================================
# MAIN
# ============================================================================
case "$1" in
scan)
cmd_scan
;;
conflicts)
load_config
cmd_conflicts
;;
fix-port)
cmd_fix_port "$2" "$3"
;;
status)
cmd_status
;;
tor)
case "$2" in
add) cmd_tor_add "$3" "$4" "$5" ;;
list) cmd_tor_list ;;
remove) cmd_tor_remove "$3" ;;
sync) cmd_tor_sync ;;
*) log_err "Usage: secubox-exposure tor {add|list|remove|sync}"; exit 1 ;;
esac
;;
ssl)
case "$2" in
add) cmd_ssl_add "$3" "$4" "$5" ;;
list) cmd_ssl_list ;;
remove) cmd_ssl_remove "$3" ;;
*) log_err "Usage: secubox-exposure ssl {add|list|remove}"; exit 1 ;;
esac
;;
help|--help|-h)
cmd_help
;;
*)
cmd_help
exit 1
;;
esac