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