secubox-openwrt/package/secubox/secubox-app-exposure/files/usr/sbin/secubox-exposure
CyberMind-FR 53af5ac2e9 feat(exposure): Add emancipate/revoke commands for multi-channel exposure
- `emancipate <service> <port> <domain> [--tor] [--dns] [--mesh] [--all]`
  Unified multi-channel exposure: Tor + DNS/SSL + Mesh in single command
- `revoke <service> [--tor] [--dns] [--mesh] [--all]`
  Removes exposure from selected channels
- Enhanced `status` command shows emancipated services with active channels
- UCI tracking for emancipation state (port, domain, channel flags)
- Integrates with dnsctl, haproxyctl, and secubox-p2p

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 04:18:33 +01:00

1062 lines
34 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
}
# ============================================================================
# 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 <service> <port> <domain> [--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 <service> [--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 <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
emancipate <svc> <port> <domain> [flags]
Expose service through multiple channels
Flags: --tor, --dns, --mesh, --all (default)
revoke <svc> [flags] Remove service exposure
Flags: --tor, --dns, --mesh, --all (default)
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:
# 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