#!/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 [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 [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 " 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 [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 " 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 [options] COMMANDS: scan Scan all listening services conflicts Detect port conflicts fix-port [port] Change service port (auto-assigns if no port given) status Show exposure status summary tor add [port] Create Tor hidden service tor list List hidden services tor remove Remove hidden service tor sync Sync hidden services to Tor Shield ssl add Add HAProxy SSL backend ssl list List SSL backends ssl remove 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