#!/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 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" 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 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" # Restart Tor /etc/init.d/tor restart 2>/dev/null || systemctl restart tor 2>/dev/null log_ok "Hidden service removed" } # ============================================================================ # HAPROXY SSL BACKENDS # ============================================================================ 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 HAProxy config exists if [ ! -f "$HAPROXY_CONFIG" ]; then log_err "HAProxy config not found: $HAPROXY_CONFIG" return 1 fi # Check if already configured if grep -q "backend ${service}_backend" "$HAPROXY_CONFIG"; then log_warn "Backend for $service already exists in HAProxy config" return 0 fi log_info "Adding SSL backend for $service ($domain -> 127.0.0.1:$local_port)" # Create backend config local backend_config=" # Backend for $service (added by secubox-exposure) backend ${service}_backend mode http option httpchk GET / http-request set-header X-Forwarded-Proto https server ${service} 127.0.0.1:$local_port check " # Add ACL to https frontend local acl_line=" acl host_${service} hdr(host) -i $domain" local use_line=" use_backend ${service}_backend if host_${service}" # Check if https-in frontend exists if grep -q "frontend https-in" "$HAPROXY_CONFIG"; then # Add ACL and use_backend before the default_backend line sed -i "/frontend https-in/,/default_backend/ { /default_backend/ i\\ $acl_line\\ $use_line }" "$HAPROXY_CONFIG" else log_warn "No https-in frontend found. Adding basic HTTPS frontend." cat >> "$HAPROXY_CONFIG" << EOF frontend https-in bind *:443 ssl crt $HAPROXY_CERTS/ mode http option httplog $acl_line $use_line default_backend default_backend EOF fi # Add backend at end of file echo "$backend_config" >> "$HAPROXY_CONFIG" # Save to UCI 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 backend added for $service" log_info "Domain: $domain -> 127.0.0.1:$local_port" log_warn "Note: You need to add SSL certificate for $domain to $HAPROXY_CERTS/" log_info "Reloading HAProxy..." # Reload HAProxy (in LXC container) if [ -x "/usr/sbin/haproxyctl" ]; then /usr/sbin/haproxyctl reload else lxc-attach -n haproxy -- /etc/init.d/haproxy reload 2>/dev/null || \ /etc/init.d/haproxy reload 2>/dev/null fi log_ok "SSL backend configured" } cmd_ssl_list() { load_config log_info "HAProxy SSL Backends:" echo "" printf "%-15s %-30s %-20s\n" "SERVICE" "DOMAIN" "BACKEND" printf "%-15s %-30s %-20s\n" "---------------" "------------------------------" "--------------------" # Parse from HAProxy config if [ -f "$HAPROXY_CONFIG" ]; then grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" | while read line; do local backend=$(echo "$line" | awk '{print $2}') local service=$(echo "$backend" | sed 's/_backend$//') # Get domain from ACL local domain=$(grep "acl host_${service} " "$HAPROXY_CONFIG" | awk '{print $NF}') # Get server line local server=$(grep -A5 "backend $backend" "$HAPROXY_CONFIG" | grep "server " | awk '{print $3}') printf "%-15s %-30s %-20s\n" "$service" "${domain:-N/A}" "${server:-N/A}" done fi echo "" } cmd_ssl_remove() { local service="$1" if [ -z "$service" ]; then log_err "Usage: secubox-exposure ssl remove " return 1 fi load_config if [ ! -f "$HAPROXY_CONFIG" ]; then log_err "HAProxy config not found" return 1 fi if ! grep -q "backend ${service}_backend" "$HAPROXY_CONFIG"; then log_err "No backend found for $service" return 1 fi log_info "Removing SSL backend for $service" # Remove ACL and use_backend lines sed -i "/acl host_${service} /d" "$HAPROXY_CONFIG" sed -i "/use_backend ${service}_backend/d" "$HAPROXY_CONFIG" # Remove backend block sed -i "/# Backend for $service/,/^$/d" "$HAPROXY_CONFIG" sed -i "/^backend ${service}_backend$/,/^$/d" "$HAPROXY_CONFIG" # Update UCI uci delete "${CONFIG_NAME}.${service}.ssl" 2>/dev/null uci delete "${CONFIG_NAME}.${service}.ssl_domain" 2>/dev/null uci commit "$CONFIG_NAME" # Reload HAProxy if [ -x "/usr/sbin/haproxyctl" ]; then /usr/sbin/haproxyctl reload else lxc-attach -n haproxy -- /etc/init.d/haproxy reload 2>/dev/null || \ /etc/init.d/haproxy reload 2>/dev/null fi 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 local ssl_backends=0 [ -f "$HAPROXY_CONFIG" ] && ssl_backends=$(grep -c "^backend.*_backend$" "$HAPROXY_CONFIG" 2>/dev/null || echo 0) echo -e "${BLUE}HAProxy SSL Backends:${NC} $ssl_backends" if [ "$ssl_backends" -gt 0 ] && [ -f "$HAPROXY_CONFIG" ]; then grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" | while read line; do local backend=$(echo "$line" | awk '{print $2}' | sed 's/_backend$//') echo " - $backend" done fi 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 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" ;; *) log_err "Usage: secubox-exposure tor {add|list|remove}"; 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