#!/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" } # ============================================================================ # EMANCIPATE - Unified Multi-Channel Exposure # ============================================================================ cmd_emancipate() { local service="$1" local port="$2" local domain="$3" shift 3 2>/dev/null || true if [ -z "$service" ] || [ -z "$port" ]; then log_err "Usage: secubox-exposure emancipate [--tor] [--dns] [--mesh] [--all]" return 1 fi local enable_tor=0 enable_dns=0 enable_mesh=0 # Parse flags while [ $# -gt 0 ]; do case "$1" in --tor) enable_tor=1 ;; --dns) enable_dns=1 ;; --mesh) enable_mesh=1 ;; --all) enable_tor=1; enable_dns=1; enable_mesh=1 ;; esac shift done # Default to --all if no flags specified if [ "$enable_tor" = "0" ] && [ "$enable_dns" = "0" ] && [ "$enable_mesh" = "0" ]; then enable_tor=1 enable_dns=1 enable_mesh=1 fi load_config # Validate: check if port is listening local listen_info=$(netstat -tlnp 2>/dev/null | grep ":$port ") if [ -z "$listen_info" ]; then log_warn "Port $port is not currently listening (service may start later)" else # Check if localhost-only local listen_addr=$(echo "$listen_info" | awk '{print $4}' | head -1) case "$listen_addr" in 127.0.0.1:*|::1:*) log_err "Cannot expose localhost-only service (listening on $listen_addr)" log_err "Service must bind to 0.0.0.0 or a specific LAN IP" return 1 ;; esac fi echo "" log_info "Emancipating ${CYAN}$service${NC} on port $port" echo "" local success=0 local failed=0 # Channel 1: Tor Hidden Service if [ "$enable_tor" = "1" ]; then log_info "Channel 1: Tor Hidden Service..." if cmd_tor_add "$service" "$port" 80 2>/dev/null; then local onion=$(cat "$TOR_HIDDEN_DIR/$service/hostname" 2>/dev/null) if [ -n "$onion" ]; then echo -e " ${GREEN}✓${NC} Tor: http://$onion" success=$((success + 1)) else echo -e " ${YELLOW}⏳${NC} Tor: Generating onion address..." success=$((success + 1)) fi else echo -e " ${RED}✗${NC} Tor: Failed to create hidden service" failed=$((failed + 1)) fi fi # Channel 2: DNS/SSL via HAProxy if [ "$enable_dns" = "1" ]; then if [ -z "$domain" ]; then echo -e " ${YELLOW}⚠${NC} DNS: Skipped (no domain specified)" else log_info "Channel 2: DNS/SSL via HAProxy..." # Check for dnsctl if [ -x "/usr/sbin/dnsctl" ]; then # Get public IP local public_ip=$(curl -s --connect-timeout 5 ifconfig.me 2>/dev/null || \ curl -s --connect-timeout 5 icanhazip.com 2>/dev/null || \ echo "") if [ -n "$public_ip" ]; then # Extract subdomain from FQDN (e.g., gitea.example.com -> gitea) local subdomain="${domain%%.*}" # Create DNS A record if /usr/sbin/dnsctl add A "$subdomain" "$public_ip" 2>/dev/null; then log_ok "DNS A record created: $subdomain -> $public_ip" else log_warn "DNS A record creation failed (may already exist)" fi else log_warn "Could not determine public IP for DNS record" fi else log_warn "dnsctl not found - skipping DNS record creation" fi # Create HAProxy vhost + backend if cmd_ssl_add "$service" "$domain" "$port" 2>/dev/null; then echo -e " ${GREEN}✓${NC} DNS: https://$domain" success=$((success + 1)) # Request certificate if haproxyctl supports it if [ -x "/usr/sbin/haproxyctl" ]; then log_info "Requesting SSL certificate for $domain..." /usr/sbin/haproxyctl cert add "$domain" 2>/dev/null || \ log_warn "Certificate request may have failed (check haproxyctl logs)" fi else echo -e " ${RED}✗${NC} DNS: Failed to create HAProxy backend" failed=$((failed + 1)) fi fi fi # Channel 3: Mesh P2P if [ "$enable_mesh" = "1" ]; then log_info "Channel 3: Mesh P2P..." if [ -x "/usr/sbin/secubox-p2p" ]; then if /usr/sbin/secubox-p2p publish "$service" "$port" "$service" 2>/dev/null; then echo -e " ${GREEN}✓${NC} Mesh: http://${service}.mesh.local" success=$((success + 1)) # Update mesh DNS /usr/sbin/secubox-p2p dns-update 2>/dev/null || true else echo -e " ${RED}✗${NC} Mesh: Failed to publish service" failed=$((failed + 1)) fi else echo -e " ${YELLOW}⚠${NC} Mesh: secubox-p2p not installed" fi fi # Store emancipation state in UCI uci set "${CONFIG_NAME}.${service}=service" uci set "${CONFIG_NAME}.${service}.port=$port" [ -n "$domain" ] && uci set "${CONFIG_NAME}.${service}.domain=$domain" uci set "${CONFIG_NAME}.${service}.tor=$enable_tor" uci set "${CONFIG_NAME}.${service}.dns=$enable_dns" uci set "${CONFIG_NAME}.${service}.mesh=$enable_mesh" uci set "${CONFIG_NAME}.${service}.emancipated=1" uci commit "$CONFIG_NAME" echo "" if [ "$failed" -eq 0 ]; then log_ok "Emancipation complete: $success channel(s) active" else log_warn "Emancipation partial: $success succeeded, $failed failed" fi echo "" } cmd_revoke() { local service="$1" shift 2>/dev/null || true if [ -z "$service" ]; then log_err "Usage: secubox-exposure revoke [--tor] [--dns] [--mesh] [--all]" return 1 fi local enable_tor=0 enable_dns=0 enable_mesh=0 # Parse flags while [ $# -gt 0 ]; do case "$1" in --tor) enable_tor=1 ;; --dns) enable_dns=1 ;; --mesh) enable_mesh=1 ;; --all) enable_tor=1; enable_dns=1; enable_mesh=1 ;; esac shift done # Default to --all if no flags specified if [ "$enable_tor" = "0" ] && [ "$enable_dns" = "0" ] && [ "$enable_mesh" = "0" ]; then enable_tor=1 enable_dns=1 enable_mesh=1 fi load_config echo "" log_info "Revoking exposure for ${CYAN}$service${NC}" echo "" local success=0 # Channel 1: Remove Tor Hidden Service if [ "$enable_tor" = "1" ]; then if [ -d "$TOR_HIDDEN_DIR/$service" ]; then log_info "Removing Tor hidden service..." cmd_tor_remove "$service" 2>/dev/null echo -e " ${GREEN}✓${NC} Tor: Hidden service removed" success=$((success + 1)) else echo -e " ${YELLOW}−${NC} Tor: No hidden service found" fi fi # Channel 2: Remove DNS/SSL if [ "$enable_dns" = "1" ]; then local domain=$(uci -q get "${CONFIG_NAME}.${service}.domain") if [ -n "$domain" ]; then log_info "Removing DNS/SSL..." # Remove DNS record if [ -x "/usr/sbin/dnsctl" ]; then local subdomain="${domain%%.*}" /usr/sbin/dnsctl rm A "$subdomain" 2>/dev/null || true fi # Remove HAProxy vhost/backend cmd_ssl_remove "$service" 2>/dev/null echo -e " ${GREEN}✓${NC} DNS: HAProxy vhost removed" success=$((success + 1)) # Remove certificate if [ -x "/usr/sbin/haproxyctl" ]; then /usr/sbin/haproxyctl cert remove "$domain" 2>/dev/null || true fi else echo -e " ${YELLOW}−${NC} DNS: No domain configured" fi fi # Channel 3: Remove from Mesh if [ "$enable_mesh" = "1" ]; then if [ -x "/usr/sbin/secubox-p2p" ]; then log_info "Removing from mesh..." /usr/sbin/secubox-p2p unpublish "$service" 2>/dev/null || true /usr/sbin/secubox-p2p dns-update 2>/dev/null || true echo -e " ${GREEN}✓${NC} Mesh: Service unpublished" success=$((success + 1)) else echo -e " ${YELLOW}−${NC} Mesh: secubox-p2p not installed" fi fi # Update UCI if [ "$enable_tor" = "1" ] && [ "$enable_dns" = "1" ] && [ "$enable_mesh" = "1" ]; then # Full revoke - delete the entire section uci delete "${CONFIG_NAME}.${service}" 2>/dev/null else # Partial revoke - just update flags [ "$enable_tor" = "1" ] && uci set "${CONFIG_NAME}.${service}.tor=0" [ "$enable_dns" = "1" ] && uci set "${CONFIG_NAME}.${service}.dns=0" [ "$enable_mesh" = "1" ] && uci set "${CONFIG_NAME}.${service}.mesh=0" # Check if all channels are now disabled local tor_enabled=$(uci -q get "${CONFIG_NAME}.${service}.tor" || echo "0") local dns_enabled=$(uci -q get "${CONFIG_NAME}.${service}.dns" || echo "0") local mesh_enabled=$(uci -q get "${CONFIG_NAME}.${service}.mesh" || echo "0") if [ "$tor_enabled" = "0" ] && [ "$dns_enabled" = "0" ] && [ "$mesh_enabled" = "0" ]; then uci set "${CONFIG_NAME}.${service}.emancipated=0" fi fi uci commit "$CONFIG_NAME" echo "" log_ok "Revocation complete: $success channel(s) removed" echo "" } # ============================================================================ # 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 "" # Emancipated services echo -e "${BLUE}Emancipated Services:${NC}" local emancipated=0 for svc in $(uci show "$CONFIG_NAME" 2>/dev/null | grep "=service$" | cut -d'.' -f2 | cut -d'=' -f1); do local is_emancipated=$(uci -q get "${CONFIG_NAME}.${svc}.emancipated") [ "$is_emancipated" != "1" ] && continue local port=$(uci -q get "${CONFIG_NAME}.${svc}.port") local domain=$(uci -q get "${CONFIG_NAME}.${svc}.domain") local tor=$(uci -q get "${CONFIG_NAME}.${svc}.tor") local dns=$(uci -q get "${CONFIG_NAME}.${svc}.dns") local mesh=$(uci -q get "${CONFIG_NAME}.${svc}.mesh") local channels="" [ "$tor" = "1" ] && channels="${channels}Tor " [ "$dns" = "1" ] && channels="${channels}DNS " [ "$mesh" = "1" ] && channels="${channels}Mesh" echo " - ${svc} (:${port}) [${channels}]" [ -n "$domain" ] && echo " Domain: ${domain}" emancipated=$((emancipated + 1)) done [ "$emancipated" = "0" ] && echo " (none)" 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 emancipate [flags] Expose service through multiple channels Flags: --tor, --dns, --mesh, --all (default) revoke [flags] Remove service exposure Flags: --tor, --dns, --mesh, --all (default) 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: # Full emancipation (Tor + DNS + Mesh) secubox-exposure emancipate gitea 3000 gitea.example.com --all # Tor-only exposure secubox-exposure emancipate streamlit 8501 "" --tor # DNS/SSL only secubox-exposure emancipate ollama 11434 ai.example.com --dns # Revoke all channels secubox-exposure revoke gitea --all # Revoke only Tor secubox-exposure revoke gitea --tor # Legacy commands secubox-exposure scan secubox-exposure tor add gitea secubox-exposure ssl add gitea git.example.com EOF } # ============================================================================ # MAIN # ============================================================================ case "$1" in scan) cmd_scan ;; conflicts) load_config cmd_conflicts ;; fix-port) cmd_fix_port "$2" "$3" ;; status) cmd_status ;; emancipate) shift cmd_emancipate "$@" ;; revoke) shift cmd_revoke "$@" ;; 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